ref impl : yt-dlp wrapper

This commit is contained in:
lovebird 2025-03-10 14:45:38 +01:00
parent 261736a139
commit 7cce1cc6c2
9 changed files with 2870 additions and 0 deletions

View File

@ -0,0 +1,327 @@
# yt-dlp-wrapper
A TypeScript wrapper library for [yt-dlp](https://github.com/yt-dlp/yt-dlp), a powerful command-line video downloader.
[![NPM Version](https://img.shields.io/npm/v/yt-dlp-wrapper.svg)](https://www.npmjs.com/package/yt-dlp-wrapper)
[![License](https://img.shields.io/npm/l/yt-dlp-wrapper.svg)](https://github.com/yourusername/yt-dlp-wrapper/blob/main/LICENSE)
## Features
- 🔄 **Full TypeScript support** with comprehensive type definitions
- 🧩 **Modular architecture** for easy integration into your projects
- 🔍 **Zod validation** for reliable input/output handling
- 📊 **Structured logging** with TSLog
- 🛠️ **Command-line interface** built with yargs
- ⚡ **Promise-based API** for easy async operations
- 🔧 **Highly configurable** with sensible defaults
- 🔄 **Progress tracking** for downloads
- 💼 **Error handling** with clear error messages
## Prerequisites
Before using this library, ensure you have:
1. **Node.js** (v14 or higher)
2. **yt-dlp** installed on your system:
- **Linux/macOS**: `brew install yt-dlp` or `pip install yt-dlp`
- **Windows**: Download from [yt-dlp GitHub releases](https://github.com/yt-dlp/yt-dlp/releases)
## Installation
```bash
# Using npm
npm install yt-dlp-wrapper
# Using yarn
yarn add yt-dlp-wrapper
# Using pnpm
pnpm add yt-dlp-wrapper
```
## CLI Usage
The library includes a command-line interface for common operations:
### Download a video
```bash
# Basic download
npx yt-dlp-wrapper download https://www.youtube.com/watch?v=dQw4w9WgXcQ
# Download with specific format
npx yt-dlp-wrapper download https://www.tiktok.com/@businessblurb/video/7479849082844892458 -f "best[height<=720]"
# Download to specific directory
npx yt-dlp-wrapper download https://youtu.be/dQw4w9WgXcQ --output-dir "./downloads"
```
### Get video information
```bash
# Basic info
npx yt-dlp-wrapper info https://www.youtube.com/watch?v=dQw4w9WgXcQ
# With JSON output
npx yt-dlp-wrapper info https://www.tiktok.com/@businessblurb/video/7479849082844892458 --dump-json
```
### List available formats
```bash
npx yt-dlp-wrapper formats https://www.youtube.com/watch?v=dQw4w9WgXcQ
```
### Help
```bash
# General help
npx yt-dlp-wrapper --help
# Command-specific help
npx yt-dlp-wrapper download --help
```
## API Usage
### Basic Usage
```typescript
import { YtDlp } from 'yt-dlp-wrapper';
// Create a new instance with default options
const ytdlp = new YtDlp();
// Download a video
async function downloadVideo() {
try {
const filePath = await ytdlp.downloadVideo('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
console.log(`Video downloaded to: ${filePath}`);
} catch (error) {
console.error('Download failed:', error);
}
}
downloadVideo();
```
### Download with Options
```typescript
import { YtDlp } from 'yt-dlp-wrapper';
const ytdlp = new YtDlp();
async function downloadWithOptions() {
try {
const filePath = await ytdlp.downloadVideo('https://www.tiktok.com/@businessblurb/video/7479849082844892458', {
format: 'bestvideo[height<=720]+bestaudio/best[height<=720]',
outputDir: './downloads',
filename: 'tiktok-video.mp4',
subtitles: true,
audioOnly: false,
// Add any other options as needed
});
console.log(`Video downloaded to: ${filePath}`);
} catch (error) {
console.error('Download failed:', error);
}
}
downloadWithOptions();
```
### Get Video Information
```typescript
import { YtDlp } from 'yt-dlp-wrapper';
const ytdlp = new YtDlp();
async function getVideoInfo() {
try {
const info = await ytdlp.getVideoInfo('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
console.log('Video title:', info.title);
console.log('Duration:', info.duration);
console.log('Uploader:', info.uploader);
// Access other properties as needed
} catch (error) {
console.error('Failed to get video info:', error);
}
}
getVideoInfo();
```
### List Available Formats
```typescript
import { YtDlp } from 'yt-dlp-wrapper';
const ytdlp = new YtDlp();
async function listFormats() {
try {
const formats = await ytdlp.listFormats('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
// Display all formats
formats.forEach(format => {
console.log(`Format ID: ${format.formatId}`);
console.log(`Resolution: ${format.resolution}`);
console.log(`Extension: ${format.extension}`);
console.log(`File size: ${format.filesize}`);
console.log('---');
});
} catch (error) {
console.error('Failed to list formats:', error);
}
}
listFormats();
```
### Custom Configuration
```typescript
import { YtDlp } from 'yt-dlp-wrapper';
// Create a new instance with custom options
const ytdlp = new YtDlp({
executablePath: '/usr/local/bin/yt-dlp', // Custom path to yt-dlp executable
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
verbose: true,
// Add other global options as needed
});
// Use as normal
ytdlp.downloadVideo('https://www.youtube.com/watch?v=dQw4w9WgXcQ')
.then(filePath => console.log(`Video downloaded to: ${filePath}`))
.catch(error => console.error('Download failed:', error));
```
## Advanced Examples
### Download Playlist
```typescript
import { YtDlp } from 'yt-dlp-wrapper';
const ytdlp = new YtDlp();
async function downloadPlaylist() {
try {
const result = await ytdlp.downloadVideo('https://www.youtube.com/playlist?list=PLexamplelistID', {
outputDir: './playlists',
playlistItems: '1-5', // Only download the first 5 videos
limit: 5,
format: 'best[height<=480]' // Lower quality to save space
});
console.log(`Playlist downloaded to: ${result}`);
} catch (error) {
console.error('Playlist download failed:', error);
}
}
downloadPlaylist();
```
### Progress Tracking
```typescript
import { YtDlp } from 'yt-dlp-wrapper';
const ytdlp = new YtDlp();
async function downloadWithProgress() {
try {
const filePath = await ytdlp.downloadVideo('https://www.youtube.com/watch?v=dQw4w9WgXcQ', {
onProgress: (progress) => {
console.log(`Download progress: ${progress.percent}%`);
console.log(`Speed: ${progress.speed}`);
console.log(`ETA: ${progress.eta}`);
}
});
console.log(`Video downloaded to: ${filePath}`);
} catch (error) {
console.error('Download failed:', error);
}
}
downloadWithProgress();
```
## Error Handling
The library provides detailed error information:
```typescript
import { YtDlp, YtDlpError } from 'yt-dlp-wrapper';
const ytdlp = new YtDlp();
async function handleErrors() {
try {
await ytdlp.downloadVideo('https://invalid-url.com/video');
} catch (error) {
if (error instanceof YtDlpError) {
console.error(`YtDlp Error: ${error.message}`);
console.error(`Error Code: ${error.code}`);
console.error(`Command: ${error.command}`);
} else {
console.error('Unknown error:', error);
}
}
}
handleErrors();
```
## API Reference
### YtDlp Class
#### Constructor
```typescript
new YtDlp(options?: YtDlpOptions)
```
**YtDlpOptions:**
| Option | Type | Description | Default |
|--------|------|-------------|---------|
| executablePath | string | Path to yt-dlp executable | 'yt-dlp' |
| userAgent | string | User agent to use for requests | Default browser UA |
| verbose | boolean | Enable verbose output | false |
| quiet | boolean | Suppress output | false |
#### Methods
**downloadVideo(url, options?)**
Downloads a video from the given URL.
**getVideoInfo(url, options?)**
Gets information about a video.
**listFormats(url, options?)**
Lists available formats for a video.
**checkInstallation()**
Verifies that yt-dlp is installed and working.
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
This project is licensed under the MIT License - see the LICENSE file for details.
## Acknowledgments
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) - The amazing tool this library wraps
- All the contributors to the open-source libraries used in this project

View File

@ -0,0 +1,49 @@
{
"name": "yt-dlp-wrapper",
"version": "1.0.0",
"description": "TypeScript wrapper for yt-dlp with CLI interface",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"yt-dlp-wrapper": "dist/cli.js"
},
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"start": "node dist/cli.js",
"dev": "ts-node --esm src/cli.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"yt-dlp",
"video",
"downloader",
"cli"
],
"author": "",
"license": "ISC",
"dependencies": {
"@types/node": "^22.13.10",
"@types/yargs": "^17.0.33",
"ts-node": "^10.9.2",
"tslog": "^4.9.3",
"typescript": "^5.8.2",
"yargs": "^17.7.2",
"zod": "^3.24.2"
},
"engines": {
"node": ">=16.0.0"
},
"devDependencies": {
"@vitest/coverage-v8": "^3.0.8",
"@vitest/ui": "^3.0.8",
"vitest": "^3.0.8"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,183 @@
#!/usr/bin/env node
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { YtDlp } from './ytdlp.js';
import { logger } from './logger.js';
import { FormatOptionsSchema, VideoInfoOptionsSchema, DownloadOptionsSchema } from './types.js';
import { z } from 'zod';
const ytdlp = new YtDlp();
const cli = yargs(hideBin(process.argv))
.scriptName('ytdlp-ts')
.usage('$0 <cmd> [args]')
.command(
'download [url]',
'Download a video',
(yargs) => {
return yargs
.positional('url', {
type: 'string',
describe: 'URL of the video to download',
demandOption: true,
})
.option('format', {
type: 'string',
describe: 'Video format code',
alias: 'f',
})
.option('output', {
type: 'string',
describe: 'Output filename template',
alias: 'o',
})
.option('quiet', {
type: 'boolean',
describe: 'Activate quiet mode',
alias: 'q',
default: false,
})
.option('verbose', {
type: 'boolean',
describe: 'Print various debugging information',
alias: 'v',
default: false,
});
},
async (argv) => {
try {
logger.info(`Downloading video from ${argv.url}`);
// Parse and validate options using Zod
const options = DownloadOptionsSchema.parse({
format: argv.format,
output: argv.output,
quiet: argv.quiet,
verbose: argv.verbose,
});
await ytdlp.downloadVideo(argv.url as string, options);
logger.info('Download completed successfully');
} catch (error) {
if (error instanceof z.ZodError) {
logger.error('Invalid options:', error.errors);
} else {
logger.error('Failed to download video:', error);
}
process.exit(1);
}
}
)
.command(
'info [url]',
'Get video information',
(yargs) => {
return yargs
.positional('url', {
type: 'string',
describe: 'URL of the video',
demandOption: true,
})
.option('dump-json', {
type: 'boolean',
describe: 'Output JSON information',
default: false,
})
.option('flat-playlist', {
type: 'boolean',
describe: 'Flat playlist output',
default: false,
});
},
async (argv) => {
try {
logger.info(`Getting info for video: ${argv.url}`);
// Parse and validate options using Zod
const options = VideoInfoOptionsSchema.parse({
dumpJson: argv.dumpJson,
flatPlaylist: argv.flatPlaylist,
});
const info = await ytdlp.getVideoInfo(argv.url as string, options);
console.log(JSON.stringify(info, null, 2));
} catch (error) {
if (error instanceof z.ZodError) {
logger.error('Invalid options:', error.errors);
} else {
logger.error('Failed to get video info:', error);
}
process.exit(1);
}
}
)
.command(
'formats [url]',
'List available formats of a video',
(yargs) => {
return yargs
.positional('url', {
type: 'string',
describe: 'URL of the video',
demandOption: true,
})
.option('all', {
type: 'boolean',
describe: 'Show all available formats',
default: false,
});
},
async (argv) => {
try {
logger.info(`Getting available formats for video: ${argv.url}`);
// Parse and validate options using Zod
const options = FormatOptionsSchema.parse({
all: argv.all,
});
const formats = await ytdlp.listFormats(argv.url as string, options);
console.log(formats);
} catch (error) {
if (error instanceof z.ZodError) {
logger.error('Invalid options:', error.errors);
} else {
logger.error('Failed to list formats:', error);
}
process.exit(1);
}
}
)
.example(
'$0 download https://www.tiktok.com/@businessblurb/video/7479849082844892458',
'Download a TikTok video'
)
.example(
'$0 download https://youtu.be/dQw4w9WgXcQ -f "bestvideo[height<=720]+bestaudio/best[height<=720]"',
'Download YouTube video in 720p or lower quality'
)
.example(
'$0 info https://www.tiktok.com/@businessblurb/video/7479849082844892458 --dump-json',
'Get TikTok video info as JSON'
)
.example(
'$0 formats https://youtu.be/dQw4w9WgXcQ',
'List available formats for a YouTube video'
)
.demandCommand(1, 'You need to specify a command')
.strict()
.help()
.alias('h', 'help')
.version()
.alias('V', 'version')
.wrap(100); // Fixed width value instead of yargs.terminalWidth() which isn't compatible with ESM
// If this file is run directly
if (import.meta.url === `file://${process.argv[1]}`) {
// Execute the CLI
cli.parse();
}
// Export for use as a library
export default cli;

View File

@ -0,0 +1,28 @@
// Export YtDlp class
export { YtDlp } from './ytdlp.js';
// Export all types and schemas
export {
// Core types
YtDlpOptions,
DownloadOptions,
FormatOptions,
VideoInfoOptions,
VideoFormat,
VideoInfo,
// Zod schemas
YtDlpOptionsSchema,
DownloadOptionsSchema,
FormatOptionsSchema,
VideoInfoOptionsSchema,
VideoFormatSchema,
VideoInfoSchema,
} from './types.js';
// Export logger
export { logger } from './logger.js';
// Re-export CLI for direct usage
export { default as cli } from './cli.js';

View File

@ -0,0 +1,55 @@
import { Logger, ILogObj } from 'tslog';
// Configure log levels
export enum LogLevel {
SILLY = 'silly',
TRACE = 'trace',
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error',
FATAL = 'fatal'
}
// Mapping from string LogLevel to numeric values expected by tslog
const logLevelToTsLogLevel: Record<LogLevel, number> = {
[LogLevel.SILLY]: 0,
[LogLevel.TRACE]: 1,
[LogLevel.DEBUG]: 2,
[LogLevel.INFO]: 3,
[LogLevel.WARN]: 4,
[LogLevel.ERROR]: 5,
[LogLevel.FATAL]: 6
};
// Convert a LogLevel string to its corresponding numeric value
const getNumericLogLevel = (level: LogLevel): number => {
return logLevelToTsLogLevel[level];
};
// Custom transport for logs if needed
const logToTransport = (logObject: ILogObj) => {
// Here you can implement custom transport like file or external service
// For example, log to file or send to a log management service
// console.log("Custom transport:", JSON.stringify(logObject));
};
// Create the logger instance
export const logger = new Logger({
name: "yt-dlp-wrapper"
});
// Add transport if needed
// logger.attachTransport(
// {
// silly: logToTransport,
// debug: logToTransport,
// trace: logToTransport,
// info: logToTransport,
// warn: logToTransport,
// error: logToTransport,
// fatal: logToTransport,
// },
// LogLevel.INFO
// );
export default logger;

View File

@ -0,0 +1,224 @@
import { z } from 'zod';
// Basic YouTube DLP options schema
export const YtDlpOptionsSchema = z.object({
// Path to the yt-dlp executable
executablePath: z.string().optional(),
// Output options
output: z.string().optional(),
format: z.string().optional(),
formatSort: z.string().optional(),
mergeOutputFormat: z.enum(['mp4', 'flv', 'webm', 'mkv', 'avi']).optional(),
// Download options
limit: z.number().int().positive().optional(),
maxFilesize: z.string().optional(),
minFilesize: z.string().optional(),
// Filesystem options
noOverwrites: z.boolean().optional(),
continue: z.boolean().optional(),
noPart: z.boolean().optional(),
// Thumbnail options
writeThumbnail: z.boolean().optional(),
writeAllThumbnails: z.boolean().optional(),
// Subtitles options
writeSubtitles: z.boolean().optional(),
writeAutoSubtitles: z.boolean().optional(),
subLang: z.string().optional(),
// Authentication options
username: z.string().optional(),
password: z.string().optional(),
// Video selection options
playlistStart: z.number().int().positive().optional(),
playlistEnd: z.number().int().positive().optional(),
playlistItems: z.string().optional(),
// Post-processing options
extractAudio: z.boolean().optional(),
audioFormat: z.enum(['best', 'aac', 'flac', 'mp3', 'm4a', 'opus', 'vorbis', 'wav']).optional(),
audioQuality: z.string().optional(),
remuxVideo: z.enum(['mp4', 'mkv', 'flv', 'webm', 'mov', 'avi']).optional(),
recodeVideo: z.enum(['mp4', 'flv', 'webm', 'mkv', 'avi']).optional(),
// Verbosity and simulation options
quiet: z.boolean().optional(),
verbose: z.boolean().optional(),
noWarnings: z.boolean().optional(),
simulate: z.boolean().optional(),
// Workarounds
noCheckCertificates: z.boolean().optional(),
preferInsecure: z.boolean().optional(),
userAgent: z.string().optional(),
// Extra arguments as string array
extraArgs: z.array(z.string()).optional(),
});
// Type derived from the schema
export type YtDlpOptions = z.infer<typeof YtDlpOptionsSchema>;
// Video information schema
export const VideoInfoSchema = z.object({
id: z.string(),
title: z.string(),
formats: z.array(
z.object({
format_id: z.string(),
format: z.string(),
ext: z.string(),
resolution: z.string().optional(),
fps: z.number().optional(),
filesize: z.number().optional(),
tbr: z.number().optional(),
protocol: z.string(),
vcodec: z.string(),
acodec: z.string(),
})
),
thumbnails: z.array(
z.object({
url: z.string(),
height: z.number().optional(),
width: z.number().optional(),
})
).optional(),
description: z.string().optional(),
upload_date: z.string().optional(),
uploader: z.string().optional(),
uploader_id: z.string().optional(),
uploader_url: z.string().optional(),
channel_id: z.string().optional(),
channel_url: z.string().optional(),
duration: z.number().optional(),
view_count: z.number().optional(),
like_count: z.number().optional(),
dislike_count: z.number().optional(),
average_rating: z.number().optional(),
age_limit: z.number().optional(),
webpage_url: z.string(),
categories: z.array(z.string()).optional(),
tags: z.array(z.string()).optional(),
is_live: z.boolean().optional(),
was_live: z.boolean().optional(),
playable_in_embed: z.boolean().optional(),
availability: z.string().optional(),
});
export type VideoInfo = z.infer<typeof VideoInfoSchema>;
// Download result schema
export const DownloadResultSchema = z.object({
videoInfo: VideoInfoSchema,
filePath: z.string(),
downloadedBytes: z.number().optional(),
elapsedTime: z.number().optional(),
averageSpeed: z.number().optional(), // in bytes/s
success: z.boolean(),
error: z.string().optional(),
});
export type DownloadResult = z.infer<typeof DownloadResultSchema>;
// Command execution result schema
export const CommandResultSchema = z.object({
command: z.string(),
stdout: z.string(),
stderr: z.string(),
success: z.boolean(),
exitCode: z.number(),
});
export type CommandResult = z.infer<typeof CommandResultSchema>;
// Progress update schema for download progress events
export const ProgressUpdateSchema = z.object({
videoId: z.string(),
percent: z.number().min(0).max(100),
totalSize: z.number().optional(),
downloadedBytes: z.number(),
speed: z.number(), // in bytes/s
eta: z.number().optional(), // in seconds
status: z.enum(['downloading', 'finished', 'error']),
message: z.string().optional(),
});
export type ProgressUpdate = z.infer<typeof ProgressUpdateSchema>;
// Error types
export enum YtDlpErrorType {
PROCESS_ERROR = 'PROCESS_ERROR',
VALIDATION_ERROR = 'VALIDATION_ERROR',
DOWNLOAD_ERROR = 'DOWNLOAD_ERROR',
UNSUPPORTED_URL = 'UNSUPPORTED_URL',
NETWORK_ERROR = 'NETWORK_ERROR',
}
// Custom error schema
export const YtDlpErrorSchema = z.object({
type: z.nativeEnum(YtDlpErrorType),
message: z.string(),
details: z.record(z.any()).optional(),
command: z.string().optional(),
});
export type YtDlpError = z.infer<typeof YtDlpErrorSchema>;
// Download options schema
export const DownloadOptionsSchema = z.object({
outputDir: z.string().optional(),
format: z.string().optional(),
outputTemplate: z.string().optional(),
audioOnly: z.boolean().optional(),
audioFormat: z.string().optional(),
subtitles: z.union([z.boolean(), z.array(z.string())]).optional(),
maxFileSize: z.number().optional(),
rateLimit: z.string().optional(),
additionalArgs: z.array(z.string()).optional(),
});
export type DownloadOptions = z.infer<typeof DownloadOptionsSchema>;
// Format options schema for listing video formats
export const FormatOptionsSchema = z.object({
all: z.boolean().optional().default(false),
});
export type FormatOptions = z.infer<typeof FormatOptionsSchema>;
// Video info options schema
export const VideoInfoOptionsSchema = z.object({
dumpJson: z.boolean().optional().default(false),
flatPlaylist: z.boolean().optional().default(false),
});
export type VideoInfoOptions = z.infer<typeof VideoInfoOptionsSchema>;
// Video format schema representing a single format option returned by yt-dlp
export const VideoFormatSchema = z.object({
format_id: z.string(),
format: z.string(),
ext: z.string(),
resolution: z.string().optional(),
fps: z.number().optional(),
filesize: z.number().optional(),
tbr: z.number().optional(),
protocol: z.string(),
vcodec: z.string(),
acodec: z.string(),
width: z.number().optional(),
height: z.number().optional(),
url: z.string().optional(),
format_note: z.string().optional(),
container: z.string().optional(),
quality: z.number().optional(),
preference: z.number().optional(),
});
export type VideoFormat = z.infer<typeof VideoFormatSchema>;

View File

@ -0,0 +1,245 @@
import { exec, spawn } from 'node:child_process';
import { promisify } from 'node:util';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { VideoInfo, DownloadOptions, YtDlpOptions, FormatOptions, VideoInfoOptions } from './types.js';
import { logger } from './logger.js';
const execAsync = promisify(exec);
/**
* A wrapper class for the yt-dlp command line tool
*/
export class YtDlp {
private executable: string = 'yt-dlp';
/**
* Create a new YtDlp instance
* @param options Configuration options for yt-dlp
*/
constructor(private options: YtDlpOptions = {}) {
if (options.executablePath) {
this.executable = options.executablePath;
}
logger.debug('YtDlp initialized with options:', options);
}
/**
* Check if yt-dlp is installed and accessible
* @returns Promise resolving to true if yt-dlp is installed, false otherwise
*/
async isInstalled(): Promise<boolean> {
try {
const { stdout } = await execAsync(`${this.executable} --version`);
logger.debug(`yt-dlp version: ${stdout.trim()}`);
return true;
} catch (error) {
logger.warn('yt-dlp is not installed or not found in PATH');
return false;
}
}
/**
* Download a video from a given URL
* @param url The URL of the video to download
* @param options Download options
* @returns Promise resolving to the path of the downloaded file
*/
async downloadVideo(url: string, options: DownloadOptions = {}): Promise<string> {
if (!url) {
throw new Error('URL is required');
}
logger.info(`Downloading video from: ${url}`);
// Prepare output directory
const outputDir = options.outputDir || '.';
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Build command arguments
const args: string[] = [];
// Format selection
if (options.format) {
args.push('-f', options.format);
}
// Output template
const outputTemplate = options.outputTemplate || '%(title)s.%(ext)s';
args.push('-o', path.join(outputDir, outputTemplate));
// Add other options
if (options.audioOnly) {
args.push('-x');
if (options.audioFormat) {
args.push('--audio-format', options.audioFormat);
}
}
if (options.subtitles) {
args.push('--write-subs');
if (Array.isArray(options.subtitles)) {
args.push('--sub-lang', options.subtitles.join(','));
}
}
if (options.maxFileSize) {
args.push('--max-filesize', options.maxFileSize.toString());
}
if (options.rateLimit) {
args.push('--limit-rate', options.rateLimit);
}
// Add custom arguments if provided
if (options.additionalArgs) {
args.push(...options.additionalArgs);
}
// Add the URL
args.push(url);
logger.debug('Executing command:', `${this.executable} ${args.join(' ')}`);
return new Promise((resolve, reject) => {
const ytdlpProcess = spawn(this.executable, args, { stdio: ['ignore', 'pipe', 'pipe'] });
let stdout = '';
let stderr = '';
let downloadedFile: string | null = null;
ytdlpProcess.stdout.on('data', (data) => {
const output = data.toString();
stdout += output;
logger.debug(output.trim());
// Try to extract the output filename
const destinationMatch = output.match(/Destination: (.+)/);
if (destinationMatch && destinationMatch[1]) {
downloadedFile = destinationMatch[1].trim();
}
// Alternative method to extract the output filename
const alreadyDownloadedMatch = output.match(/\[download\] (.+) has already been downloaded/);
if (alreadyDownloadedMatch && alreadyDownloadedMatch[1]) {
downloadedFile = alreadyDownloadedMatch[1].trim();
}
});
ytdlpProcess.stderr.on('data', (data) => {
const output = data.toString();
stderr += output;
logger.error(output.trim());
});
ytdlpProcess.on('close', (code) => {
if (code === 0) {
if (downloadedFile) {
logger.info(`Successfully downloaded: ${downloadedFile}`);
resolve(downloadedFile);
} else {
// Try to find the downloaded file from stdout if it wasn't captured
const fileMatch = stdout.match(/\[download\] (.+?) has already been downloaded/);
if (fileMatch && fileMatch[1]) {
logger.info(`Successfully downloaded: ${fileMatch[1]}`);
resolve(fileMatch[1]);
} else {
logger.info('Download successful, but could not determine the output file name');
resolve('Download completed');
}
}
} else {
reject(new Error(`yt-dlp exited with code ${code}: ${stderr}`));
}
});
ytdlpProcess.on('error', (err) => {
logger.error('Failed to start yt-dlp process:', err);
reject(new Error(`Failed to start yt-dlp: ${err.message}`));
});
});
}
/**
* Get information about a video without downloading it
* @param url The URL of the video to get information for
* @returns Promise resolving to video information
*/
async getVideoInfo(url: string, options: VideoInfoOptions = { dumpJson: false, flatPlaylist: false }): Promise<VideoInfo> {
if (!url) {
throw new Error('URL is required');
}
logger.info(`Getting video info for: ${url}`);
try {
// Build command with options
const args: string[] = ['--dump-json'];
// Add user agent if specified in global options
if (this.options.userAgent) {
args.push('--user-agent', this.options.userAgent);
}
// Add VideoInfoOptions flags
if (options.flatPlaylist) {
args.push('--flat-playlist');
}
args.push(url);
const { stdout } = await execAsync(`${this.executable} ${args.join(' ')}`);
const videoInfo = JSON.parse(stdout);
logger.debug('Video info retrieved successfully');
return videoInfo;
} catch (error) {
logger.error('Failed to get video info:', error);
throw new Error(`Failed to get video info: ${(error as Error).message}`);
}
}
/**
* List available formats for a video
* @param url The URL of the video to get formats for
* @returns Promise resolving to a string containing format information
*/
async listFormats(url: string, options: FormatOptions = { all: false }): Promise<string> {
if (!url) {
throw new Error('URL is required');
}
logger.info(`Listing formats for: ${url}`);
try {
// Build command with options
const formatFlag = options.all ? '--list-formats-all' : '-F';
const { stdout } = await execAsync(`${this.executable} ${formatFlag} ${url}`);
logger.debug('Format list retrieved successfully');
return stdout;
} catch (error) {
logger.error('Failed to list formats:', error);
throw new Error(`Failed to list formats: ${(error as Error).message}`);
}
}
/**
* Set the path to the yt-dlp executable
* @param path Path to the yt-dlp executable
*/
setExecutablePath(path: string): void {
if (!path) {
throw new Error('Executable path cannot be empty');
}
this.executable = path;
logger.debug(`yt-dlp executable path set to: ${path}`);
}
}
export default YtDlp;

View File

@ -0,0 +1,118 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"lib": ["es2020", "dom"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "libReplacement": true, /* Enable lib replacement. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "NodeNext", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
"moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
"resolveJsonModule": true, /* Enable importing .json files. */
"moduleDetection": "force", /* Control what method is used to detect module-format JS files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
"declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
"allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"],
"ts-node": {
"esm": true
}
}