diff --git a/.gitignore b/.gitignore index 90f0d2a..f3899bd 100644 --- a/.gitignore +++ b/.gitignore @@ -31,5 +31,5 @@ test-dist # WebStorm .idea/ - +plan/ dist/ diff --git a/README.md b/README.md index 7344c7c..53ba80b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/cookies.md b/docs/cookies.md new file mode 100644 index 0000000..dfb646c --- /dev/null +++ b/docs/cookies.md @@ -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) diff --git a/package.json b/package.json index 9334408..8679e43 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts new file mode 100644 index 0000000..25bbb48 --- /dev/null +++ b/src/__tests__/config.test.ts @@ -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); + }); + }); +}); diff --git a/src/__tests__/search.test.ts b/src/__tests__/search.test.ts index 3c8508e..96e6b45 100644 --- a/src/__tests__/search.test.ts +++ b/src/__tests__/search.test.ts @@ -5,11 +5,11 @@ import { searchVideos } from '../modules/search.js'; import { CONFIG } from '../config.js'; 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:'); expect(result).toContain('Duration:'); @@ -20,33 +20,33 @@ 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'); - + // Count number of video entries (each video has a numbered entry) const count1 = (result1.match(/^\d+\./gm) || []).length; const count5 = (result5.match(/^\d+\./gm) || []).length; - + expect(count1).toBe(1); expect(count5).toBe(5); }, 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?:/); expect(result).toMatch(/\d+\. \*\*.*\*\*/); // Numbered list with bold titles @@ -58,8 +58,8 @@ 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 expect(typeof result).toBe('string'); diff --git a/src/config.ts b/src/config.ts index 3593dd7..fbc675b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,10 +1,21 @@ import * as os from "os"; import * as path from "path"; +import * as fs from "fs"; type DeepPartial = { [P in keyof T]?: T[P] extends object ? DeepPartial : 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 { envConfig.download = downloadConfig; } + // Cookie configuration + const cookiesConfig: Partial = {}; + 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 { 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(); \ No newline at end of file diff --git a/src/index.mts b/src/index.mts index 7985284..69e7512 100644 --- a/src/index.mts +++ b/src/index.mts @@ -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") { diff --git a/src/modules/audio.ts b/src/modules/audio.ts index e4a340f..49c114b 100644 --- a/src/modules/audio.ts +++ b/src/modules/audio.ts @@ -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 { +export async function listSubtitles(url: string, config?: Config): Promise { 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 ]); diff --git a/src/modules/video.ts b/src/modules/video.ts index 3ec8643..44b5a7b 100644 --- a/src/modules/video.ts +++ b/src/modules/video.ts @@ -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