feat(cookies): add cookie support for authenticated access

- Add YTDLP_COOKIES_FILE and YTDLP_COOKIES_FROM_BROWSER env vars
- Support all yt-dlp cookie methods (file, browser extraction)
- Validate browser names (brave, chrome, chromium, edge, firefox, opera, safari, vivaldi, whale)
- Cookie file takes precedence over browser extraction
- Add getCookieArgs() helper function
- Integrate cookie args into all modules (video, audio, subtitle, search, metadata)
- Add comprehensive cookie documentation (docs/cookies.md)
- Add 12 unit tests for cookie configuration
- Fix search.test.ts function signature issue

Closes #19
This commit is contained in:
kevinwatt 2025-12-06 18:26:34 +08:00
parent 26b2137751
commit 87ba2f8494
13 changed files with 646 additions and 31 deletions

2
.gitignore vendored
View File

@ -31,5 +31,5 @@ test-dist
# WebStorm
.idea/
plan/
dist/

View File

@ -287,6 +287,7 @@ Get human-readable metadata summary
- **[API Reference](./docs/api.md)** - Detailed tool documentation
- **[Configuration](./docs/configuration.md)** - Environment variables and settings
- **[Cookie Configuration](./docs/cookies.md)** - Authentication and private video access
- **[Error Handling](./docs/error-handling.md)** - Common errors and solutions
- **[Contributing](./docs/contributing.md)** - How to contribute
@ -313,6 +314,38 @@ YTDLP_CHARACTER_LIMIT=25000
YTDLP_MAX_TRANSCRIPT_LENGTH=50000
```
### Cookie Configuration
To access private videos, age-restricted content, or avoid rate limits, configure cookies:
```bash
# Extract cookies from browser (recommended)
YTDLP_COOKIES_FROM_BROWSER=chrome
# Or use a cookie file
YTDLP_COOKIES_FILE=/path/to/cookies.txt
```
**MCP Configuration with cookies:**
```json
{
"mcpServers": {
"yt-dlp": {
"command": "npx",
"args": ["-y", "@kevinwatt/yt-dlp-mcp"],
"env": {
"YTDLP_COOKIES_FROM_BROWSER": "chrome"
}
}
}
}
```
Supported browsers: `brave`, `chrome`, `chromium`, `edge`, `firefox`, `opera`, `safari`, `vivaldi`, `whale`
See [Cookie Configuration Guide](./docs/cookies.md) for detailed setup instructions.
---
## 🏗️ Architecture

302
docs/cookies.md Normal file
View File

@ -0,0 +1,302 @@
# Cookies Configuration Guide
## Why Do You Need Cookies?
You need to configure cookies for yt-dlp-mcp in the following situations:
- **Access private videos**: Videos that require login to view
- **Age-restricted content**: Content requiring account age verification
- **Bypass CAPTCHA**: Some websites require verification
- **Avoid rate limiting**: Reduce HTTP 429 (Too Many Requests) errors
- **YouTube Premium features**: Access premium-exclusive content and quality
## Configuration Methods
yt-dlp-mcp supports two cookie configuration methods via environment variables.
### Method 1: Extract from Browser (Recommended)
This is the simplest approach. yt-dlp reads cookies directly from your browser.
```bash
YTDLP_COOKIES_FROM_BROWSER=chrome
```
#### Supported Browsers
| Browser | Value |
|---------|-------|
| Google Chrome | `chrome` |
| Chromium | `chromium` |
| Microsoft Edge | `edge` |
| Mozilla Firefox | `firefox` |
| Brave | `brave` |
| Opera | `opera` |
| Safari (macOS) | `safari` |
| Vivaldi | `vivaldi` |
| Whale | `whale` |
#### Advanced Configuration
```bash
# Specify Chrome Profile
YTDLP_COOKIES_FROM_BROWSER=chrome:Profile 1
# Specify Firefox Container
YTDLP_COOKIES_FROM_BROWSER=firefox::work
# Flatpak-installed Chrome (Linux)
YTDLP_COOKIES_FROM_BROWSER=chrome:~/.var/app/com.google.Chrome/
# Full format: BROWSER:PROFILE::CONTAINER
YTDLP_COOKIES_FROM_BROWSER=chrome:Profile 1::personal
```
### Method 2: Use Cookie File
If you prefer using a fixed cookie file, or automatic extraction doesn't work:
```bash
YTDLP_COOKIES_FILE=/path/to/cookies.txt
```
The cookie file must be in Netscape/Mozilla format with the first line:
```
# Netscape HTTP Cookie File
```
## Exporting Cookies
### Using yt-dlp (Recommended)
This is the most reliable method, ensuring correct format:
```bash
# Export from Chrome
yt-dlp --cookies-from-browser chrome --cookies cookies.txt "https://www.youtube.com"
# Export from Firefox
yt-dlp --cookies-from-browser firefox --cookies cookies.txt "https://www.youtube.com"
```
> **Note**: This command exports ALL website cookies from your browser. Keep this file secure.
### Using Browser Extensions
| Browser | Extension |
|---------|-----------|
| Chrome | [Get cookies.txt LOCALLY](https://chrome.google.com/webstore/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc) |
| Firefox | [cookies.txt](https://addons.mozilla.org/firefox/addon/cookies-txt/) |
> **Warning**: Only use the recommended extensions above. Some cookie export extensions may be malware.
## MCP Configuration Examples
### Claude Desktop
Edit `claude_desktop_config.json`:
**Using Browser Cookies:**
```json
{
"mcpServers": {
"yt-dlp": {
"command": "npx",
"args": ["@kevinwatt/yt-dlp-mcp"],
"env": {
"YTDLP_COOKIES_FROM_BROWSER": "chrome"
}
}
}
}
```
**Using Cookie File:**
```json
{
"mcpServers": {
"yt-dlp": {
"command": "npx",
"args": ["@kevinwatt/yt-dlp-mcp"],
"env": {
"YTDLP_COOKIES_FILE": "/Users/username/.config/yt-dlp/cookies.txt"
}
}
}
}
```
### Configuration File Locations
| OS | Claude Desktop Config Location |
|----|-------------------------------|
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
| Linux | `~/.config/Claude/claude_desktop_config.json` |
## Priority Order
When both `YTDLP_COOKIES_FILE` and `YTDLP_COOKIES_FROM_BROWSER` are set:
1. `YTDLP_COOKIES_FILE` is used first
2. If no file is set, `YTDLP_COOKIES_FROM_BROWSER` is used
## Security Best Practices
### Cookie File Security
1. **Keep it safe**: Cookie files contain your login credentials; leakage may lead to account compromise
2. **Never share**: Never share cookie files with others or upload to public locations
3. **Version control**: Add `cookies.txt` to `.gitignore`
4. **File permissions**:
```bash
chmod 600 cookies.txt # Owner read/write only
```
### Browser Cookie Extraction
- Ensure your browser is up to date
- You may need to close the browser temporarily during extraction
- Some browser security features may block cookie extraction
### Regular Updates
- Browser cookies expire
- Re-export cookies periodically
- If you encounter authentication errors, try updating cookies
## Troubleshooting
### Error: Cookie file not found
```
Error: Cookie file not found: /path/to/cookies.txt
```
**Solutions:**
1. Verify the file path is correct
2. Confirm the file exists
3. Ensure the MCP service has permission to read the file
### Error: Browser cookies could not be loaded
```
Error: Could not load cookies from chrome
```
**Solutions:**
1. Verify browser name spelling is correct
2. Try closing the browser and retry
3. Ensure no multiple browser instances are running
4. Check if browser has password-protected cookie storage
### Error: Invalid cookie file format
```
Error: Invalid cookie file format
```
**Solutions:**
1. Ensure first line is `# Netscape HTTP Cookie File` or `# HTTP Cookie File`
2. Check line ending format (Unix uses LF, Windows uses CRLF)
3. Re-export cookies using yt-dlp
### Still Cannot Access Private Videos
1. **Confirm login**: Verify you're logged in to the video platform in your browser
2. **Refresh page**: Refresh the video page in browser before exporting
3. **Re-export**: Re-export cookies
4. **Check permissions**: Confirm your account has permission to access the video
### HTTP 400: Bad Request
This usually indicates incorrect line ending format in the cookie file.
**Linux/macOS:**
```bash
# Convert to Unix line endings
sed -i 's/\r$//' cookies.txt
```
**Windows:**
Use Notepad++ or VS Code to convert line endings to LF.
## YouTube JavaScript Runtime Requirement
YouTube requires a JavaScript runtime for yt-dlp to function properly. Without it, you may see errors like:
```
WARNING: [youtube] Signature solving failed: Some formats may be missing
ERROR: Requested format is not available
```
### Installing EJS (Recommended)
EJS is a lightweight JavaScript runtime specifically designed for yt-dlp.
**Linux (Debian/Ubuntu):**
```bash
# Install Node.js if not already installed
sudo apt install nodejs
# Install EJS globally
sudo npm install -g @aspect-build/ejs
```
**Linux (Arch):**
```bash
sudo pacman -S nodejs npm
sudo npm install -g @aspect-build/ejs
```
**macOS:**
```bash
brew install node
npm install -g @aspect-build/ejs
```
**Windows:**
```powershell
# Install Node.js from https://nodejs.org/
npm install -g @aspect-build/ejs
```
### Alternative: PhantomJS
If EJS doesn't work, you can try PhantomJS:
**Linux:**
```bash
sudo apt install phantomjs
```
**macOS:**
```bash
brew install phantomjs
```
### Verifying Installation
Test that yt-dlp can use the JavaScript runtime:
```bash
yt-dlp --dump-json "https://www.youtube.com/watch?v=dQw4w9WgXcQ" 2>&1 | head -1
```
If successful, you should see JSON output starting with `{`.
### Additional Dependencies for Cookie Extraction (Linux)
On Linux, cookie extraction from browsers requires the `secretstorage` module:
```bash
python3 -m pip install secretstorage
```
This is needed to decrypt cookies stored by Chromium-based browsers.
## Related Links
- [yt-dlp Cookie FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp)
- [yt-dlp EJS Wiki](https://github.com/yt-dlp/yt-dlp/wiki/EJS)
- [yt-dlp Documentation](https://github.com/yt-dlp/yt-dlp#readme)

View File

@ -1,6 +1,6 @@
{
"name": "@kevinwatt/yt-dlp-mcp",
"version": "0.7.0",
"version": "0.8.0",
"description": "An MCP server implementation that integrates with yt-dlp, providing video and audio content download capabilities (e.g. YouTube, Facebook, Tiktok, etc.) for LLMs.",
"keywords": [
"mcp",

View File

@ -0,0 +1,179 @@
// @ts-nocheck
// @jest-environment node
import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
// Store original env
const originalEnv = { ...process.env };
describe('Cookie Configuration', () => {
beforeEach(() => {
// Reset environment before each test
process.env = { ...originalEnv };
// Clear module cache to reload config
jest.resetModules();
});
afterEach(() => {
process.env = originalEnv;
});
describe('getCookieArgs', () => {
test('returns empty array when no cookies configured', async () => {
const { getCookieArgs, loadConfig } = await import('../config.js');
const config = loadConfig();
const args = getCookieArgs(config);
expect(args).toEqual([]);
});
test('returns --cookies args when file is configured', async () => {
// Create a temporary cookie file
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cookie-test-'));
const cookieFile = path.join(tempDir, 'cookies.txt');
fs.writeFileSync(cookieFile, '# Netscape HTTP Cookie File\n');
process.env.YTDLP_COOKIES_FILE = cookieFile;
const { getCookieArgs, loadConfig } = await import('../config.js');
const config = loadConfig();
const args = getCookieArgs(config);
expect(args).toEqual(['--cookies', cookieFile]);
// Cleanup
fs.rmSync(tempDir, { recursive: true, force: true });
});
test('returns --cookies-from-browser args when browser is configured', async () => {
process.env.YTDLP_COOKIES_FROM_BROWSER = 'chrome';
const { getCookieArgs, loadConfig } = await import('../config.js');
const config = loadConfig();
const args = getCookieArgs(config);
expect(args).toEqual(['--cookies-from-browser', 'chrome']);
});
test('file takes precedence over browser', async () => {
// Create a temporary cookie file
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cookie-test-'));
const cookieFile = path.join(tempDir, 'cookies.txt');
fs.writeFileSync(cookieFile, '# Netscape HTTP Cookie File\n');
process.env.YTDLP_COOKIES_FILE = cookieFile;
process.env.YTDLP_COOKIES_FROM_BROWSER = 'chrome';
const { getCookieArgs, loadConfig } = await import('../config.js');
const config = loadConfig();
const args = getCookieArgs(config);
expect(args).toEqual(['--cookies', cookieFile]);
// Cleanup
fs.rmSync(tempDir, { recursive: true, force: true });
});
test('supports browser with profile', async () => {
process.env.YTDLP_COOKIES_FROM_BROWSER = 'chrome:Profile 1';
const { getCookieArgs, loadConfig } = await import('../config.js');
const config = loadConfig();
const args = getCookieArgs(config);
expect(args).toEqual(['--cookies-from-browser', 'chrome:Profile 1']);
});
test('supports browser with container', async () => {
process.env.YTDLP_COOKIES_FROM_BROWSER = 'firefox::work';
const { getCookieArgs, loadConfig } = await import('../config.js');
const config = loadConfig();
const args = getCookieArgs(config);
expect(args).toEqual(['--cookies-from-browser', 'firefox::work']);
});
});
describe('Cookie Validation', () => {
test('clears invalid cookie file path with warning', async () => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
process.env.YTDLP_COOKIES_FILE = '/nonexistent/path/cookies.txt';
const { loadConfig } = await import('../config.js');
const config = loadConfig();
expect(config.cookies.file).toBeUndefined();
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Cookie file not found')
);
consoleSpy.mockRestore();
});
test('accepts valid browser names', async () => {
const validBrowsers = ['brave', 'chrome', 'chromium', 'edge', 'firefox', 'opera', 'safari', 'vivaldi', 'whale'];
for (const browser of validBrowsers) {
jest.resetModules();
process.env = { ...originalEnv };
process.env.YTDLP_COOKIES_FROM_BROWSER = browser;
const { loadConfig } = await import('../config.js');
const config = loadConfig();
expect(config.cookies.fromBrowser).toBe(browser);
}
});
test('clears invalid browser name with warning', async () => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
process.env.YTDLP_COOKIES_FROM_BROWSER = 'invalidbrowser';
const { loadConfig } = await import('../config.js');
const config = loadConfig();
expect(config.cookies.fromBrowser).toBeUndefined();
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Invalid browser name')
);
consoleSpy.mockRestore();
});
test('accepts valid browser with custom path (Flatpak style)', async () => {
// Path format is valid for Flatpak installations
process.env.YTDLP_COOKIES_FROM_BROWSER = 'chrome:~/.var/app/com.google.Chrome/';
const { loadConfig } = await import('../config.js');
const config = loadConfig();
expect(config.cookies.fromBrowser).toBe('chrome:~/.var/app/com.google.Chrome/');
});
test('accepts valid browser with empty profile', async () => {
// chrome: is valid (empty profile means default)
process.env.YTDLP_COOKIES_FROM_BROWSER = 'chrome:';
const { loadConfig } = await import('../config.js');
const config = loadConfig();
expect(config.cookies.fromBrowser).toBe('chrome:');
});
});
describe('VALID_BROWSERS constant', () => {
test('exports valid browsers list', async () => {
const { VALID_BROWSERS } = await import('../config.js');
expect(VALID_BROWSERS).toContain('chrome');
expect(VALID_BROWSERS).toContain('firefox');
expect(VALID_BROWSERS).toContain('edge');
expect(VALID_BROWSERS).toContain('safari');
expect(VALID_BROWSERS.length).toBe(9);
});
});
});

View File

@ -8,7 +8,7 @@ describe('Search functionality tests', () => {
describe('searchVideos', () => {
test('should successfully search for JavaScript tutorials', async () => {
const result = await searchVideos('javascript tutorial', 3, CONFIG);
const result = await searchVideos('javascript tutorial', 3, 0, 'markdown', CONFIG);
expect(result).toContain('Found 3 videos');
expect(result).toContain('Channel:');
@ -20,18 +20,18 @@ describe('Search functionality tests', () => {
}, 30000); // Increase timeout for real network calls
test('should reject empty search queries', async () => {
await expect(searchVideos('', 10, CONFIG)).rejects.toThrow('Search query cannot be empty');
await expect(searchVideos(' ', 10, CONFIG)).rejects.toThrow('Search query cannot be empty');
await expect(searchVideos('', 10, 0, 'markdown', CONFIG)).rejects.toThrow('Search query cannot be empty');
await expect(searchVideos(' ', 10, 0, 'markdown', CONFIG)).rejects.toThrow('Search query cannot be empty');
});
test('should validate maxResults parameter range', async () => {
await expect(searchVideos('test', 0, CONFIG)).rejects.toThrow('Number of results must be between 1 and 50');
await expect(searchVideos('test', 51, CONFIG)).rejects.toThrow('Number of results must be between 1 and 50');
await expect(searchVideos('test', 0, 0, 'markdown', CONFIG)).rejects.toThrow('Number of results must be between 1 and 50');
await expect(searchVideos('test', 51, 0, 'markdown', CONFIG)).rejects.toThrow('Number of results must be between 1 and 50');
});
test('should handle search with different result counts', async () => {
const result1 = await searchVideos('python programming', 1, CONFIG);
const result5 = await searchVideos('python programming', 5, CONFIG);
const result1 = await searchVideos('python programming', 1, 0, 'markdown', CONFIG);
const result5 = await searchVideos('python programming', 5, 0, 'markdown', CONFIG);
expect(result1).toContain('Found 1 video');
expect(result5).toContain('Found 5 videos');
@ -45,7 +45,7 @@ describe('Search functionality tests', () => {
}, 30000);
test('should return properly formatted results', async () => {
const result = await searchVideos('react tutorial', 2, CONFIG);
const result = await searchVideos('react tutorial', 2, 0, 'markdown', CONFIG);
// Check for proper formatting
expect(result).toMatch(/Found \d+ videos?:/);
@ -58,7 +58,7 @@ describe('Search functionality tests', () => {
test('should handle obscure search terms gracefully', async () => {
// Using a very specific and unlikely search term
const result = await searchVideos('asdfghjklqwertyuiopzxcvbnm12345', 1, CONFIG);
const result = await searchVideos('asdfghjklqwertyuiopzxcvbnm12345', 1, 0, 'markdown', CONFIG);
// Even obscure terms should return some results, as YouTube's search is quite broad
// But if no results, it should be handled gracefully

View File

@ -1,10 +1,21 @@
import * as os from "os";
import * as path from "path";
import * as fs from "fs";
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
/**
* Valid browser names for cookie extraction
*/
export const VALID_BROWSERS = [
'brave', 'chrome', 'chromium', 'edge',
'firefox', 'opera', 'safari', 'vivaldi', 'whale'
] as const;
export type ValidBrowser = typeof VALID_BROWSERS[number];
/**
* Configuration type definitions
*/
@ -41,6 +52,13 @@ export interface Config {
characterLimit: number;
maxTranscriptLength: number;
};
// Cookie configuration for authenticated access
cookies: {
// Path to Netscape format cookie file
file?: string;
// Browser name and settings (format: BROWSER[:PROFILE][::CONTAINER])
fromBrowser?: string;
};
}
/**
@ -73,6 +91,10 @@ const defaultConfig: Config = {
limits: {
characterLimit: 25000, // Standard MCP character limit
maxTranscriptLength: 50000 // Transcripts can be larger
},
cookies: {
file: undefined,
fromBrowser: undefined
}
};
@ -123,6 +145,18 @@ function loadEnvConfig(): DeepPartial<Config> {
envConfig.download = downloadConfig;
}
// Cookie configuration
const cookiesConfig: Partial<Config['cookies']> = {};
if (process.env.YTDLP_COOKIES_FILE) {
cookiesConfig.file = process.env.YTDLP_COOKIES_FILE;
}
if (process.env.YTDLP_COOKIES_FROM_BROWSER) {
cookiesConfig.fromBrowser = process.env.YTDLP_COOKIES_FROM_BROWSER;
}
if (Object.keys(cookiesConfig).length > 0) {
envConfig.cookies = cookiesConfig;
}
return envConfig;
}
@ -159,6 +193,34 @@ function validateConfig(config: Config): void {
if (!/^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?$/i.test(config.download.defaultSubtitleLanguage)) {
throw new Error('Invalid defaultSubtitleLanguage');
}
// Validate cookies (lenient - warnings only)
validateCookiesConfig(config);
}
/**
* Validate cookie configuration (lenient - logs warnings but doesn't throw)
*/
function validateCookiesConfig(config: Config): void {
// Validate cookie file path
if (config.cookies.file) {
if (!fs.existsSync(config.cookies.file)) {
console.warn(`[yt-dlp-mcp] Cookie file not found: ${config.cookies.file}, continuing without cookies`);
config.cookies.file = undefined;
}
}
// Validate browser name only
// Format: BROWSER[:PROFILE_OR_PATH][::CONTAINER]
// We only validate browser name; yt-dlp will validate path/container
if (config.cookies.fromBrowser) {
const browserName = config.cookies.fromBrowser.split(':')[0].toLowerCase();
if (!VALID_BROWSERS.includes(browserName as ValidBrowser)) {
console.warn(`[yt-dlp-mcp] Invalid browser name: ${browserName}. Valid browsers: ${VALID_BROWSERS.join(', ')}`);
config.cookies.fromBrowser = undefined;
}
}
}
/**
@ -188,6 +250,10 @@ function mergeConfig(base: Config, override: DeepPartial<Config>): Config {
limits: {
characterLimit: override.limits?.characterLimit || base.limits.characterLimit,
maxTranscriptLength: override.limits?.maxTranscriptLength || base.limits.maxTranscriptLength
},
cookies: {
file: override.cookies?.file ?? base.cookies.file,
fromBrowser: override.cookies?.fromBrowser ?? base.cookies.fromBrowser
}
};
}
@ -225,5 +291,26 @@ export function sanitizeFilename(filename: string, config: Config['file']): stri
return safe;
}
/**
* Get cookie-related yt-dlp arguments
* Priority: file > fromBrowser
* @param config Configuration object
* @returns Array of yt-dlp arguments for cookie handling
*/
export function getCookieArgs(config: Config): string[] {
// Guard against missing cookies config
if (!config.cookies) {
return [];
}
// Cookie file takes precedence over browser extraction
if (config.cookies.file) {
return ['--cookies', config.cookies.file];
}
if (config.cookies.fromBrowser) {
return ['--cookies-from-browser', config.cookies.fromBrowser];
}
return [];
}
// Export current configuration instance
export const CONFIG = loadConfig();

View File

@ -507,7 +507,7 @@ server.setRequestHandler(
} else if (toolName === "ytdlp_list_subtitle_languages") {
const validated = ListSubtitleLanguagesSchema.parse(args);
return handleToolExecution(
() => listSubtitles(validated.url),
() => listSubtitles(validated.url, CONFIG),
"Error listing subtitle languages"
);
} else if (toolName === "ytdlp_download_video_subtitles") {

View File

@ -1,7 +1,7 @@
import { readdirSync } from "fs";
import * as path from "path";
import type { Config } from "../config.js";
import { sanitizeFilename } from "../config.js";
import { sanitizeFilename, getCookieArgs } from "../config.js";
import { _spawnPromise, validateUrl, getFormattedTimestamp, isYouTubeUrl } from "./utils.js";
/**
@ -52,6 +52,7 @@ export async function downloadAudio(url: string, config: Config): Promise<string
"--no-mtime",
"-f", format,
"--output", outputTemplate,
...getCookieArgs(config),
url
]);

View File

@ -1,4 +1,5 @@
import type { Config } from "../config.js";
import { getCookieArgs } from "../config.js";
import {
_spawnPromise,
validateUrl
@ -156,6 +157,7 @@ export async function getVideoMetadata(
"--dump-json",
"--no-warnings",
"--no-check-certificate",
...(_config ? getCookieArgs(_config) : []),
url
];

View File

@ -1,5 +1,6 @@
import { _spawnPromise } from "./utils.js";
import type { Config } from "../config.js";
import { getCookieArgs } from "../config.js";
/**
* YouTube search result interface
@ -57,7 +58,8 @@ export async function searchVideos(
"--print", "uploader",
"--print", "duration",
"--no-download",
"--quiet"
"--quiet",
...getCookieArgs(config)
];
const result = await _spawnPromise(config.tools.required[0], args);

View File

@ -2,39 +2,43 @@ import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import type { Config } from '../config.js';
import { getCookieArgs } from '../config.js';
import { _spawnPromise, validateUrl, cleanSubtitleToTranscript } from "./utils.js";
/**
* Lists all available subtitles for a video.
*
* @param url - The URL of the video
* @param config - Configuration object (optional, for cookie support)
* @returns Promise resolving to a string containing the list of available subtitles
* @throws {Error} When URL is invalid or subtitle listing fails
*
* @example
* ```typescript
* try {
* const subtitles = await listSubtitles('https://youtube.com/watch?v=...');
* const subtitles = await listSubtitles('https://youtube.com/watch?v=...', config);
* console.log('Available subtitles:', subtitles);
* } catch (error) {
* console.error('Failed to list subtitles:', error);
* }
* ```
*/
export async function listSubtitles(url: string): Promise<string> {
export async function listSubtitles(url: string, config?: Config): Promise<string> {
if (!validateUrl(url)) {
throw new Error('Invalid or unsupported URL format. Please provide a valid video URL (e.g., https://youtube.com/watch?v=...)');
}
try {
const output = await _spawnPromise('yt-dlp', [
const args = [
'--ignore-config',
'--list-subs',
'--write-auto-sub',
'--skip-download',
'--verbose',
...(config ? getCookieArgs(config) : []),
url
]);
];
const output = await _spawnPromise('yt-dlp', args);
return output;
} catch (error) {
if (error instanceof Error) {
@ -99,6 +103,7 @@ export async function downloadSubtitles(
'--sub-lang', language,
'--skip-download',
'--output', path.join(tempDir, '%(title)s.%(ext)s'),
...getCookieArgs(config),
url
]);
@ -179,6 +184,7 @@ export async function downloadTranscript(
'--sub-format', 'ttml',
'--convert-subs', 'srt',
'--output', path.join(tempDir, 'transcript.%(ext)s'),
...getCookieArgs(config),
url
]);

View File

@ -1,6 +1,6 @@
import * as path from "path";
import type { Config } from "../config.js";
import { sanitizeFilename } from "../config.js";
import { sanitizeFilename, getCookieArgs } from "../config.js";
import {
_spawnPromise,
validateUrl,
@ -102,13 +102,15 @@ export async function downloadVideo(
sanitizeFilename(`%(title)s [%(id)s] ${timestamp}`, config.file) + '.%(ext)s'
);
expectedFilename = await _spawnPromise("yt-dlp", [
const getFilenameArgs = [
"--ignore-config",
"--get-filename",
"-f", format,
"--output", outputTemplate,
...getCookieArgs(config),
url
]);
];
expectedFilename = await _spawnPromise("yt-dlp", getFilenameArgs);
expectedFilename = expectedFilename.trim();
} catch (error) {
// 如果無法獲取檔案名稱,使用隨機檔案名
@ -124,7 +126,8 @@ export async function downloadVideo(
"--newline",
"--no-mtime",
"-f", format,
"--output", outputTemplate
"--output", outputTemplate,
...getCookieArgs(config)
];
// Add trimming parameters if provided