Compare commits

...

7 Commits

Author SHA1 Message Date
4bb8b85c0e Fix dynamic YouTube cookies detection
Allow cookies uploaded after server start to be detected by checking
process.env.YOUTUBE_COOKIES_PATH dynamically instead of relying only
on the cached COOKIES_PATH constant.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 09:24:57 +00:00
14706e2589 Add public endpoint to download cookie extraction script
Features:
- New GET /public/scripts/extract-and-upload-cookies.sh endpoint
- No authentication required (public)
- Downloads the automation script directly
- Proper Content-Disposition headers for file download

Usage:
# Download the script
curl -O http://tomp3.etheryale.com:3001/public/scripts/extract-and-upload-cookies.sh

# Make executable
chmod +x extract-and-upload-cookies.sh

# Configure and run
export API_TOKEN="your_token"
export API_URL="http://tomp3.etheryale.com:3001"
./extract-and-upload-cookies.sh

Security:
- No secrets in the script (uses environment variables)
- Safe to distribute publicly
- Users provide their own API_TOKEN and API_URL

Benefits:
- One-command download
- No need to clone repo
- Easy distribution to end-users
- Cross-platform compatible (Linux/Mac/WSL)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 02:30:55 +00:00
eefe747a9e Remove HSTS header for public endpoints to allow HTTP access
Changes:
- Conditional Strict-Transport-Security header
- Only applied to protected endpoints (require auth)
- Public endpoints (/health, /api, /docs/api, /public/download/*) can use HTTP
- Fixes PowerShell/browser connection issues over HTTP

Security:
- Protected endpoints still enforce HTTPS via HSTS
- Public endpoints remain accessible over HTTP for flexibility
- Other security headers (X-Frame-Options, CSP, etc.) still apply

Resolves:
- TLS connection errors from PowerShell
- Client network socket disconnection issues
- Allows public documentation access without SSL

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 01:19:38 +00:00
c1837eca1d Add public API documentation endpoint
Features:
- New GET /docs/api endpoint to retrieve full API documentation
- Returns API.md content as JSON with markdown format
- No authentication required (public endpoint)
- Includes lastUpdated timestamp
- 26,754 characters of complete documentation

Response format:
{
  "success": true,
  "documentation": "<full markdown content>",
  "format": "markdown",
  "lastUpdated": "2025-12-08T12:12:38.942Z"
}

Use case:
- Fetch documentation programmatically
- Display in external applications
- Copy-paste into AI assistants (Claude, ChatGPT)
- Auto-generate API clients

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-08 12:38:15 +00:00
ea4c49b781 Update API documentation port from 8888 to 3001
- Changed all localhost:8888 references to localhost:3001
- Reflects actual server configuration (PORT=3001 in .env)
- 32 occurrences updated throughout the documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-08 12:13:54 +00:00
eb69e8b063 Add public download endpoint without authentication
Features:
- New GET /public/download/:filename endpoint for public file access
- No authentication required for downloading processed files
- Directory traversal protection with path.basename()
- Proper Content-Disposition headers for browser downloads

Documentation:
- Updated docs/API.md with public endpoint section
- Added to table of contents
- Usage examples and security notes

Use cases:
- Share download links via email/chat/social media
- Embed in web applications without auth
- Direct browser downloads
- Public file sharing

Security:
- Path sanitization prevents directory traversal attacks
- Only files in OUTPUT_DIR are accessible
- Returns proper 404 for non-existent files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-08 11:32:23 +00:00
751a382ccd Add YouTube cookies management system with enhanced error messages
Features:
- New POST /admin/upload-cookies endpoint for cookie upload
- Enhanced YouTube bot detection error messages with solutions
- extract-and-upload-cookies.sh script for automated cookie extraction
- Improved error display in web interface with actionable solutions
- Cookie storage in both local and persistent locations

Technical changes:
- Add enhanceYouTubeError() in youtube.js service
- Add formatYouTubeError() in web app.js
- Add handleYouTubeError() helper in server.js
- Enhanced SSE error handling for all streaming endpoints
- Updated API documentation with cookies section

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-08 10:42:53 +00:00
6 changed files with 1100 additions and 43 deletions

View File

@ -2,7 +2,7 @@
## Base URL
```
http://localhost:8888
http://localhost:3001
```
## 🔐 Authentication
@ -15,12 +15,12 @@ Include your API token in **one** of these ways:
**Option 1: X-API-Key header (Recommended)**
```bash
curl -H "X-API-Key: your_api_token_here" http://localhost:8888/endpoint
curl -H "X-API-Key: your_api_token_here" http://localhost:3001/endpoint
```
**Option 2: Authorization Bearer header**
```bash
curl -H "Authorization: Bearer your_api_token_here" http://localhost:8888/endpoint
curl -H "Authorization: Bearer your_api_token_here" http://localhost:3001/endpoint
```
### Configuration
@ -68,11 +68,14 @@ curl -H "Authorization: Bearer your_api_token_here" http://localhost:8888/endpoi
## Table of Contents
- [Authentication](#-authentication)
- [Health & Info](#health--info)
- [Public Download Endpoint](#public-download-endpoint)
- [Download Endpoints](#download-endpoints)
- [Transcription Endpoints](#transcription-endpoints)
- [Conversion Endpoints](#conversion-endpoints)
- [Translation Endpoints](#translation-endpoints)
- [Summarization Endpoints](#summarization-endpoints)
- [File Management](#file-management)
- [Admin Endpoints](#admin-endpoints)
- [Security Configuration](#security-configuration)
---
@ -82,6 +85,8 @@ curl -H "Authorization: Bearer your_api_token_here" http://localhost:8888/endpoi
### GET /health
Health check endpoint.
**Authentication**: Not required (public)
**Response:**
```json
{
@ -93,6 +98,8 @@ Health check endpoint.
### GET /api
Get API information and available endpoints.
**Authentication**: Not required (public)
**Response:**
```json
{
@ -102,6 +109,67 @@ Get API information and available endpoints.
}
```
---
## Public Download Endpoint
### GET /public/download/:filename
Public endpoint to download files without authentication.
**Authentication**: Not required (public)
**Purpose**: Share direct download links for generated files (MP3, transcriptions, translations, summaries) without requiring API authentication.
**URL Parameters:**
- `filename` (required): Name of the file to download
**Security**:
- Directory traversal protection enabled (uses `path.basename()`)
- Only files in the configured OUTPUT_DIR are accessible
- No authentication required
**Example:**
```bash
# Direct download (no auth needed)
curl -O http://localhost:3001/public/download/my_video.mp3
# Or simply open in browser
http://localhost:3001/public/download/my_video.mp3
```
**Response (Success):**
- File download with proper Content-Disposition headers
- Browser will prompt to download the file
**Response (Error - 404):**
```json
{
"error": "File not found",
"message": "File 'my_video.mp3' does not exist"
}
```
**Response (Error - 500):**
```json
{
"error": "Download failed",
"message": "Error details..."
}
```
**Use Cases:**
- Share download links via email/chat
- Embed in web applications
- Direct browser downloads
- Public file sharing
**Note**: After processing (download, transcription, etc.), use the returned `filePath` or `fileUrl` from authenticated endpoints, then construct public URL:
```
/public/download/{basename_of_filePath}
```
---
### GET /info
Get information about a YouTube video or playlist.
@ -111,7 +179,7 @@ Get information about a YouTube video or playlist.
**Example:**
```bash
curl -H "X-API-Key: your_token" \
"http://localhost:8888/info?url=https://www.youtube.com/watch?v=VIDEO_ID"
"http://localhost:3001/info?url=https://www.youtube.com/watch?v=VIDEO_ID"
```
**Response:**
@ -140,7 +208,7 @@ Download YouTube video(s) to MP3 with Server-Sent Events (SSE) progress updates.
**Example:**
```bash
curl -H "X-API-Key: your_token" \
"http://localhost:8888/download-stream?url=https://www.youtube.com/watch?v=VIDEO_ID"
"http://localhost:3001/download-stream?url=https://www.youtube.com/watch?v=VIDEO_ID"
```
**SSE Events:**
@ -164,7 +232,7 @@ Download YouTube video(s) to MP3 (non-streaming).
**Example:**
```bash
curl -H "X-API-Key: your_token" \
-X POST http://localhost:8888/download \
-X POST http://localhost:3001/download \
-H "Content-Type: application/json" \
-d '{"url":"https://www.youtube.com/watch?v=VIDEO_ID"}'
```
@ -214,7 +282,7 @@ Transcribe an existing audio file.
**Example:**
```bash
curl -H "X-API-Key: your_token" \
-X POST http://localhost:8888/transcribe \
-X POST http://localhost:3001/transcribe \
-H "Content-Type: application/json" \
-d '{
"filePath": "./output/audio.mp3",
@ -245,7 +313,7 @@ Upload and transcribe audio files.
**Example:**
```bash
curl -X POST http://localhost:8888/upload-transcribe \
curl -X POST http://localhost:3001/upload-transcribe \
-F "files=@audio1.mp3" \
-F "files=@audio2.mp3" \
-F "language=en" \
@ -282,7 +350,7 @@ Download + Transcribe with SSE progress updates.
**Example:**
```bash
curl "http://localhost:8888/process-stream?url=https://www.youtube.com/watch?v=VIDEO_ID&language=en&model=gpt-4o-mini-transcribe"
curl "http://localhost:3001/process-stream?url=https://www.youtube.com/watch?v=VIDEO_ID&language=en&model=gpt-4o-mini-transcribe"
```
**SSE Events:**
@ -309,7 +377,7 @@ Download + Transcribe (non-streaming).
**Example:**
```bash
curl -X POST http://localhost:8888/process \
curl -X POST http://localhost:3001/process \
-H "Content-Type: application/json" \
-d '{
"url": "https://www.youtube.com/watch?v=VIDEO_ID",
@ -341,6 +409,182 @@ curl -X POST http://localhost:8888/process \
}
```
### POST /upload-process
**🎯 Smart endpoint that auto-detects input and processes accordingly:**
- **Video files** (MP4, AVI, MKV, etc.) → Convert to MP3 → Transcribe
- **Audio files** (MP3, WAV, M4A, etc.) → Transcribe directly
- **URL parameter** → Download from YouTube → Transcribe
- **Mixed input** → Process both uploaded files AND URL
This endpoint intelligently handles whatever you send it!
**Form Data:**
- `files`: Video or audio file(s) (optional, multiple files supported, max 50)
- `url`: YouTube URL (optional)
- `language`: Language code for transcription (optional)
- `model`: Transcription model (optional, default: gpt-4o-mini-transcribe)
- `outputPath`: Custom output directory (optional)
**Note:** You must provide either `files`, `url`, or both.
**Example 1: Upload video files**
```bash
curl -H "X-API-Key: your_token" \
-X POST http://localhost:3001/upload-process \
-F "files=@meeting.mp4" \
-F "files=@interview.avi" \
-F "language=en" \
-F "model=gpt-4o-mini-transcribe"
```
**Example 2: Upload audio files**
```bash
curl -H "X-API-Key: your_token" \
-X POST http://localhost:3001/upload-process \
-F "files=@podcast.mp3" \
-F "files=@lecture.wav" \
-F "language=fr"
```
**Example 3: Process YouTube URL**
```bash
curl -H "X-API-Key: your_token" \
-X POST http://localhost:3001/upload-process \
-F "url=https://www.youtube.com/watch?v=VIDEO_ID" \
-F "language=en"
```
**Example 4: Mixed - Files AND URL**
```bash
curl -H "X-API-Key: your_token" \
-X POST http://localhost:3001/upload-process \
-F "files=@local_video.mp4" \
-F "url=https://www.youtube.com/watch?v=VIDEO_ID" \
-F "language=en"
```
**Response:**
```json
{
"success": true,
"totalFiles": 3,
"successCount": 3,
"failCount": 0,
"results": [
{
"success": true,
"source": "upload",
"sourceType": "video",
"fileName": "meeting.mp4",
"converted": true,
"audioPath": "./output/meeting.mp3",
"audioUrl": "/files/meeting.mp3",
"transcriptionPath": "./output/meeting.txt",
"transcriptionUrl": "/files/meeting.txt",
"text": "Transcribed content..."
},
{
"success": true,
"source": "upload",
"sourceType": "audio",
"fileName": "podcast.mp3",
"converted": false,
"audioPath": "./output/podcast.mp3",
"audioUrl": "/files/podcast.mp3",
"transcriptionPath": "./output/podcast.txt",
"transcriptionUrl": "/files/podcast.txt",
"text": "Transcribed content..."
},
{
"success": true,
"source": "url",
"sourceType": "youtube",
"title": "Video Title from YouTube",
"audioPath": "./output/video_title.mp3",
"audioUrl": "/files/video_title.mp3",
"transcriptionPath": "./output/video_title.txt",
"transcriptionUrl": "/files/video_title.txt",
"text": "Transcribed content..."
}
]
}
```
**Supported Video Formats:**
- MP4, AVI, MKV, MOV, WMV, FLV, WebM, M4V
**Supported Audio Formats:**
- MP3, WAV, M4A, FLAC, OGG, AAC
---
## Conversion Endpoints
### POST /convert-to-mp3
Upload video or audio files and convert them to MP3 format.
**Form Data:**
- `files`: Video or audio file(s) (multiple files supported, max 50)
- `bitrate`: Audio bitrate (optional, default: 192k)
- `quality`: Audio quality 0-9, where 0 is best (optional, default: 2)
**Example:**
```bash
curl -H "X-API-Key: your_token" \
-X POST http://localhost:3001/convert-to-mp3 \
-F "files=@video.mp4" \
-F "files=@another_video.avi" \
-F "bitrate=320k" \
-F "quality=0"
```
**Response:**
```json
{
"success": true,
"totalFiles": 2,
"successCount": 2,
"failCount": 0,
"results": [
{
"success": true,
"fileName": "video.mp4",
"inputPath": "./output/video.mp4",
"outputPath": "./output/video.mp3",
"outputUrl": "/files/video.mp3",
"size": "5.2 MB"
},
{
"success": true,
"fileName": "another_video.avi",
"inputPath": "./output/another_video.avi",
"outputPath": "./output/another_video.mp3",
"outputUrl": "/files/another_video.mp3",
"size": "3.8 MB"
}
]
}
```
### GET /supported-formats
Get list of supported video and audio formats for conversion.
**Example:**
```bash
curl -H "X-API-Key: your_token" \
http://localhost:3001/supported-formats
```
**Response:**
```json
{
"formats": {
"video": [".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm", ".m4v"],
"audio": [".m4a", ".wav", ".flac", ".ogg", ".aac", ".wma", ".opus"]
}
}
```
---
## Translation Endpoints
@ -377,7 +621,7 @@ Translate text.
**Example:**
```bash
curl -X POST http://localhost:8888/translate \
curl -X POST http://localhost:3001/translate \
-H "Content-Type: application/json" \
-d '{
"text": "Hello, how are you?",
@ -408,7 +652,7 @@ Translate uploaded text files.
**Example:**
```bash
curl -X POST http://localhost:8888/translate-file \
curl -X POST http://localhost:3001/translate-file \
-F "files=@document.txt" \
-F "targetLang=fr" \
-F "sourceLang=en"
@ -466,7 +710,7 @@ Summarize text using GPT-5.1.
**Example:**
```bash
curl -X POST http://localhost:8888/summarize \
curl -X POST http://localhost:3001/summarize \
-H "Content-Type: application/json" \
-d '{
"text": "Long article content...",
@ -499,7 +743,7 @@ Summarize uploaded text files using GPT-5.1.
**Example:**
```bash
curl -X POST http://localhost:8888/summarize-file \
curl -X POST http://localhost:3001/summarize-file \
-F "files=@article.txt" \
-F "style=detailed" \
-F "language=same"
@ -538,7 +782,7 @@ Full pipeline: Download -> Transcribe -> Summarize with SSE progress.
**Example:**
```bash
curl "http://localhost:8888/summarize-stream?url=https://www.youtube.com/watch?v=VIDEO_ID&style=bullet&model=gpt-4o-mini-transcribe"
curl "http://localhost:3001/summarize-stream?url=https://www.youtube.com/watch?v=VIDEO_ID&style=bullet&model=gpt-4o-mini-transcribe"
```
**SSE Events:**
@ -559,7 +803,7 @@ List all downloaded/generated files.
**Example:**
```bash
curl http://localhost:8888/files-list
curl http://localhost:3001/files-list
```
**Response:**
@ -585,7 +829,7 @@ Serve a specific file.
**Example:**
```bash
curl http://localhost:8888/files/video.mp3 --output video.mp3
curl http://localhost:3001/files/video.mp3 --output video.mp3
```
---
@ -628,6 +872,140 @@ Ensure `OPENAI_API_KEY` is set in your `.env` file for transcription, translatio
---
## Admin Endpoints
### POST /admin/upload-cookies
Upload YouTube cookies file to enable authentication bypass for bot detection.
**Purpose**: When YouTube blocks downloads with "Sign in to confirm you're not a bot", this endpoint allows you to upload cookies from your browser to authenticate requests.
**Authentication**: Required (use your API token)
**Request:**
- Method: `POST`
- Content-Type: `multipart/form-data`
- Body: File upload with field name `cookies`
**Example (cURL):**
```bash
# Upload cookies file
curl -X POST \
-H "X-API-Key: your_api_token" \
-F "cookies=@youtube-cookies.txt" \
http://localhost:3001/admin/upload-cookies
```
**Example (Using the automation script):**
```bash
# Extract cookies from browser and upload automatically
export API_TOKEN="your_api_token"
export API_URL="http://localhost:3001"
./extract-and-upload-cookies.sh
```
**Response (Success - 200):**
```json
{
"success": true,
"message": "Cookies uploaded successfully",
"paths": {
"local": "/home/user/project/youtube-cookies.txt",
"persistent": "/tmp/share/youtube-cookies.txt"
},
"note": "Cookies are now active. No restart required."
}
```
**Response (Error - 400):**
```json
{
"error": "No file uploaded",
"message": "Please upload a cookies.txt file",
"help": "Export cookies from your browser using a 'Get cookies.txt' extension"
}
```
**Response (Error - 500):**
```json
{
"error": "Failed to upload cookies",
"message": "Error details..."
}
```
### How to Get YouTube Cookies
**Method 1: Automated Script (Recommended)**
Use the provided `extract-and-upload-cookies.sh` script:
```bash
# Set your API credentials
export API_TOKEN="your_api_token"
export API_URL="http://localhost:3001"
# Run the script - it will auto-detect your browser
./extract-and-upload-cookies.sh
```
The script will:
1. Detect installed browsers (Chrome, Firefox, Edge)
2. Extract cookies using yt-dlp
3. Upload them to the API automatically
**Method 2: Manual Export**
1. **Install browser extension:**
- Chrome/Edge: [Get cookies.txt LOCALLY](https://chrome.google.com/webstore/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc)
- Firefox: [cookies.txt](https://addons.mozilla.org/en-US/firefox/addon/cookies-txt/)
2. **Login to YouTube:**
- Visit https://www.youtube.com
- Make sure you're logged into your account
3. **Export cookies:**
- Click the extension icon
- Click "Export" or "Download"
- Save the file as `youtube-cookies.txt`
4. **Upload via API:**
```bash
curl -X POST \
-H "X-API-Key: your_api_token" \
-F "cookies=@youtube-cookies.txt" \
http://localhost:3001/admin/upload-cookies
```
### Cookie Storage
Cookies are saved to two locations:
1. **Local project directory**: `/path/to/project/youtube-cookies.txt`
- Used immediately by the API
- Active without restart
2. **Persistent storage**: `/tmp/share/youtube-cookies.txt`
- Persists across server restarts
- Automatically loaded on startup (via `refresh-cookies.sh`)
### Cookie Expiration
- YouTube cookies typically expire after **2-4 weeks**
- When expired, you'll see "YouTube Bot Detection" errors
- Re-upload fresh cookies using the same method
### Security Notes
⚠️ **Important Cookie Security:**
- Cookies = Your YouTube session (treat like a password)
- Never commit `youtube-cookies.txt` to git (already in .gitignore)
- Don't share publicly
- File permissions are automatically set to `600` (owner read/write only)
- Re-export periodically when they expire
---
## Security Configuration
### Environment Variables
@ -695,13 +1073,13 @@ Before deploying to production:
```bash
# Download endpoint
curl -H "X-API-Key: your_token" \
-X POST http://localhost:8888/download \
-X POST http://localhost:3001/download \
-H "Content-Type: application/json" \
-d '{"url":"https://www.youtube.com/watch?v=VIDEO_ID"}'
# Transcribe endpoint
curl -H "X-API-Key: your_token" \
-X POST http://localhost:8888/transcribe \
-X POST http://localhost:3001/transcribe \
-H "Content-Type: application/json" \
-d '{"filePath":"./output/audio.mp3"}'
```
@ -709,7 +1087,7 @@ curl -H "X-API-Key: your_token" \
**Using Authorization Bearer:**
```bash
curl -H "Authorization: Bearer your_token" \
-X POST http://localhost:8888/summarize \
-X POST http://localhost:3001/summarize \
-H "Content-Type: application/json" \
-d '{"text":"Long text to summarize..."}'
```

135
extract-and-upload-cookies.sh Executable file
View File

@ -0,0 +1,135 @@
#!/bin/bash
# Extract and Upload Cookies - All-in-one script
# This script extracts YouTube cookies from your browser and uploads them to the API
set -e # Exit on error
# Colors for pretty output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
API_URL="${API_URL:-http://localhost:8888}"
API_TOKEN="${API_TOKEN:-}"
TEMP_COOKIES="/tmp/youtube-cookies-temp.txt"
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}YouTube Cookies Extractor & Uploader${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
# Step 1: Check if API token is set
if [ -z "$API_TOKEN" ]; then
echo -e "${YELLOW}⚠️ API_TOKEN not set in environment${NC}"
echo -e "Please enter your API token:"
read -s API_TOKEN
echo ""
fi
# Step 2: Detect available browsers
echo -e "${BLUE}🔍 Detecting browsers...${NC}"
AVAILABLE_BROWSERS=()
if command -v google-chrome &> /dev/null || command -v chromium &> /dev/null || [ -d "$HOME/.config/google-chrome" ] || [ -d "$HOME/.config/chromium" ]; then
AVAILABLE_BROWSERS+=("chrome")
fi
if command -v firefox &> /dev/null || [ -d "$HOME/.mozilla/firefox" ]; then
AVAILABLE_BROWSERS+=("firefox")
fi
if command -v microsoft-edge &> /dev/null || [ -d "$HOME/.config/microsoft-edge" ]; then
AVAILABLE_BROWSERS+=("edge")
fi
if [ ${#AVAILABLE_BROWSERS[@]} -eq 0 ]; then
echo -e "${RED}❌ No supported browsers found${NC}"
echo -e "${YELLOW}Supported browsers: Chrome, Firefox, Edge${NC}"
echo ""
echo -e "${YELLOW}Alternative: Manually export cookies${NC}"
echo -e "1. Install browser extension 'Get cookies.txt LOCALLY'"
echo -e "2. Visit youtube.com and login"
echo -e "3. Export cookies to a file"
echo -e "4. Run: curl -X POST -H \"X-API-Key: \$API_TOKEN\" -F \"cookies=@youtube-cookies.txt\" $API_URL/admin/upload-cookies"
exit 1
fi
echo -e "${GREEN}✓ Found browsers:${NC} ${AVAILABLE_BROWSERS[*]}"
echo ""
# Step 3: Let user choose browser
if [ ${#AVAILABLE_BROWSERS[@]} -eq 1 ]; then
BROWSER="${AVAILABLE_BROWSERS[0]}"
echo -e "${GREEN}Using browser: $BROWSER${NC}"
else
echo -e "${YELLOW}Multiple browsers found. Choose one:${NC}"
select BROWSER in "${AVAILABLE_BROWSERS[@]}"; do
if [ -n "$BROWSER" ]; then
break
fi
done
fi
echo ""
# Step 4: Check if yt-dlp is installed
if ! command -v yt-dlp &> /dev/null; then
echo -e "${RED}❌ yt-dlp is not installed${NC}"
echo -e "${YELLOW}Install with: pip install yt-dlp${NC}"
echo -e "${YELLOW}Or: sudo apt install yt-dlp${NC}"
exit 1
fi
# Step 5: Extract cookies using yt-dlp
echo -e "${BLUE}🍪 Extracting cookies from $BROWSER...${NC}"
if yt-dlp --cookies-from-browser "$BROWSER" --cookies "$TEMP_COOKIES" --no-download "https://www.youtube.com" > /dev/null 2>&1; then
echo -e "${GREEN}✓ Cookies extracted successfully${NC}"
else
echo -e "${RED}❌ Failed to extract cookies${NC}"
echo -e "${YELLOW}Make sure:${NC}"
echo -e " 1. You're logged into YouTube in $BROWSER"
echo -e " 2. $BROWSER is closed (some browsers lock the cookies database)"
echo -e " 3. You have the necessary permissions"
exit 1
fi
# Step 6: Upload cookies to API
echo -e "${BLUE}📤 Uploading cookies to API...${NC}"
UPLOAD_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
-H "X-API-Key: $API_TOKEN" \
-F "cookies=@$TEMP_COOKIES" \
"$API_URL/admin/upload-cookies")
HTTP_CODE=$(echo "$UPLOAD_RESPONSE" | tail -n1)
RESPONSE_BODY=$(echo "$UPLOAD_RESPONSE" | head -n-1)
if [ "$HTTP_CODE" = "200" ]; then
echo -e "${GREEN}✓ Cookies uploaded successfully!${NC}"
echo ""
echo -e "${GREEN}═══════════════════════════════════════${NC}"
echo -e "${GREEN}SUCCESS! You're all set!${NC}"
echo -e "${GREEN}═══════════════════════════════════════${NC}"
echo ""
echo "$RESPONSE_BODY" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE_BODY"
echo ""
echo -e "${BLUE} Your cookies are now active${NC}"
echo -e "${BLUE} YouTube downloads should work without bot detection${NC}"
echo -e "${BLUE} Cookies expire after a few weeks - run this script again if needed${NC}"
else
echo -e "${RED}❌ Failed to upload cookies${NC}"
echo -e "${RED}HTTP Status: $HTTP_CODE${NC}"
echo ""
echo "$RESPONSE_BODY"
exit 1
fi
# Step 7: Cleanup
rm -f "$TEMP_COOKIES"
echo ""
echo -e "${GREEN}🎉 All done!${NC}"

View File

@ -35,6 +35,67 @@ async function apiFetch(url, options = {}) {
return response;
}
// Helper: Format YouTube enhanced errors
function formatYouTubeError(errorData) {
// Check if it's an enhanced YouTube bot detection error
if (errorData.error === 'YouTube Bot Detection' || errorData.solution) {
return `
<div class="youtube-error">
<h3>🚫 ${errorData.error || 'YouTube Error'}</h3>
<p class="error-message">${errorData.message}</p>
${errorData.reason ? `<details class="error-details">
<summary>Technical Details</summary>
<pre>${errorData.reason}</pre>
</details>` : ''}
<div class="error-solution">
<h4>💡 Solution</h4>
<p><strong>${errorData.solution.quick}</strong></p>
<ol class="solution-steps">
${errorData.solution.steps.map(step => `<li>${step}</li>`).join('')}
</ol>
${errorData.solution.alternative ? `
<p class="solution-alternative"><strong>Alternative:</strong> ${errorData.solution.alternative}</p>
` : ''}
${errorData.solution.documentation ? `
<p class="solution-docs">📚 ${errorData.solution.documentation}</p>
` : ''}
</div>
${errorData.currentConfig ? `
<details class="config-status">
<summary>Current Configuration</summary>
<ul>
<li><strong>Cookies File:</strong> ${errorData.currentConfig.cookiesFile}</li>
<li><strong>Browser Extraction:</strong> ${errorData.currentConfig.cookiesBrowser}</li>
<li><strong>Status:</strong> ${errorData.currentConfig.status}</li>
</ul>
</details>
` : ''}
<div class="error-actions">
<button class="btn btn-primary btn-small" onclick="window.scrollTo(0, 0); document.getElementById('toggle-config').click();">
Upload Cookies Now
</button>
</div>
</div>
`;
}
// Generic YouTube error
if (errorData.error && errorData.error.includes('YouTube')) {
return `
<h3> ${errorData.error}</h3>
<p>${errorData.message}</p>
${errorData.solution ? `<p><em>${errorData.solution}</em></p>` : ''}
`;
}
// Default error
return `<h3>Error</h3><p>${errorData.message || errorData.error || 'Unknown error'}</p>`;
}
// API Configuration Panel
const configPanel = document.getElementById('api-config-panel');
const configHeader = document.querySelector('.config-header');
@ -363,11 +424,16 @@ document.getElementById('download-form').addEventListener('submit', async (e) =>
});
eventSource.addEventListener('error', (e) => {
let errorMsg = 'Download failed';
try { errorMsg = JSON.parse(e.data).message || errorMsg; } catch {}
let errorHtml = '<h3>Error</h3><p>Download failed</p>';
try {
const errorData = JSON.parse(e.data);
errorHtml = formatYouTubeError(errorData);
} catch {
// Keep default error
}
eventSource.close();
progressContainer.style.display = 'none';
showResult('download-result', false, `<h3>Error</h3><p>${errorMsg}</p>`);
showResult('download-result', false, errorHtml);
setLoading(button, false);
});
@ -788,11 +854,16 @@ document.getElementById('process-form').addEventListener('submit', async (e) =>
});
eventSource.addEventListener('error', (e) => {
let errorMsg = 'Processing failed';
try { errorMsg = JSON.parse(e.data).message || errorMsg; } catch {}
let errorHtml = '<h3>Error</h3><p>Processing failed</p>';
try {
const errorData = JSON.parse(e.data);
errorHtml = formatYouTubeError(errorData);
} catch {
// Keep default error
}
eventSource.close();
processProgress.style.display = 'none';
showResult('process-result', false, `<h3>Error</h3><p>${errorMsg}</p>`);
showResult('process-result', false, errorHtml);
setLoading(button, false);
});
@ -1328,11 +1399,16 @@ if (summarizeLinkForm) {
});
eventSource.addEventListener('error', (e) => {
let errorMsg = 'Processing failed';
try { errorMsg = JSON.parse(e.data).message || errorMsg; } catch {}
let errorHtml = '<h3>Error</h3><p>Processing failed</p>';
try {
const errorData = JSON.parse(e.data);
errorHtml = formatYouTubeError(errorData);
} catch {
// Keep default error
}
eventSource.close();
linkProgress.style.display = 'none';
showResult('summarize-link-result', false, `<h3>Error</h3><p>${errorMsg}</p>`);
showResult('summarize-link-result', false, errorHtml);
setLoading(button, false);
});

14
refresh-cookies.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
# Auto-refresh cookies from /tmp/share if they exist and are newer
SHARE_COOKIES="/tmp/share/youtube-cookies.txt"
LOCAL_COOKIES="/home/debian/videotomp3transcriptor/youtube-cookies.txt"
if [ -f "$SHARE_COOKIES" ]; then
# Copy if share is newer or local doesn't exist
if [ ! -f "$LOCAL_COOKIES" ] || [ "$SHARE_COOKIES" -nt "$LOCAL_COOKIES" ]; then
cp "$SHARE_COOKIES" "$LOCAL_COOKIES"
chmod 600 "$LOCAL_COOKIES"
echo "[$(date)] ✓ Cookies refreshed from /tmp/share"
fi
fi

View File

@ -87,10 +87,19 @@ app.use(express.json());
// Security headers
app.use((req, res, next) => {
// Public endpoints that should work over HTTP
const publicEndpoints = ['/health', '/api', '/docs/api'];
const isPublic = publicEndpoints.includes(req.path) || req.path.startsWith('/public/download/') || req.path.startsWith('/public/scripts/');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
// Only enforce HTTPS for protected endpoints
if (!isPublic) {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'"
@ -101,8 +110,9 @@ app.use((req, res, next) => {
// API Authentication Middleware
const authenticate = (req, res, next) => {
// Skip authentication for public endpoints
const publicEndpoints = ['/health', '/api'];
if (publicEndpoints.includes(req.path)) {
const publicEndpoints = ['/health', '/api', '/docs/api'];
// Allow public download and scripts endpoints
if (publicEndpoints.includes(req.path) || req.path.startsWith('/public/download/') || req.path.startsWith('/public/scripts/')) {
return next();
}
@ -134,6 +144,14 @@ const authenticate = (req, res, next) => {
// Apply authentication to all routes
app.use(authenticate);
// Helper function to handle YouTube enhanced errors
function handleYouTubeError(error, res, defaultMessage = 'Operation failed') {
if (error.isEnhanced && error.details) {
return res.status(503).json(error.details);
}
return res.status(500).json({ error: error.message || defaultMessage });
}
// Serve static files (HTML interface)
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -153,6 +171,7 @@ app.get('/api', (req, res) => {
'POST /download': 'Download as MP3',
'POST /transcribe': 'Transcribe audio file',
'POST /process': 'Download + transcribe',
'POST /upload-process': 'Smart: Upload video/audio OR URL -> Transcribe',
'GET /files-list': 'List downloaded files',
'GET /files/<name>': 'Serve downloaded files',
},
@ -164,6 +183,116 @@ app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
/**
* GET /docs/api
* Get complete API documentation (markdown)
*/
app.get('/docs/api', (req, res) => {
try {
const docPath = path.join(__dirname, '../docs/API.md');
if (!fs.existsSync(docPath)) {
return res.status(404).json({
error: 'Documentation not found',
message: 'API documentation file does not exist'
});
}
const docContent = fs.readFileSync(docPath, 'utf-8');
// Return as JSON with markdown content
res.json({
success: true,
documentation: docContent,
format: 'markdown',
lastUpdated: fs.statSync(docPath).mtime
});
} catch (error) {
console.error(`[API Docs] Error: ${error.message}`);
res.status(500).json({
error: 'Failed to retrieve documentation',
message: error.message
});
}
});
/**
* GET /public/scripts/extract-and-upload-cookies.sh
* Public endpoint to download the cookie extraction script
*/
app.get('/public/scripts/extract-and-upload-cookies.sh', (req, res) => {
try {
const scriptPath = path.join(__dirname, '../extract-and-upload-cookies.sh');
if (!fs.existsSync(scriptPath)) {
return res.status(404).json({
error: 'Script not found',
message: 'Cookie extraction script does not exist'
});
}
// Send file with proper headers for download
res.download(scriptPath, 'extract-and-upload-cookies.sh', (err) => {
if (err) {
console.error(`[Script Download] Error: ${err.message}`);
if (!res.headersSent) {
res.status(500).json({
error: 'Download failed',
message: err.message
});
}
}
});
} catch (error) {
console.error(`[Script Download] Error: ${error.message}`);
res.status(500).json({
error: 'Server error',
message: error.message
});
}
});
/**
* GET /public/download/:filename
* Public endpoint to download files without authentication
*/
app.get('/public/download/:filename', (req, res) => {
try {
const { filename } = req.params;
// Security: prevent directory traversal
const safeName = path.basename(filename);
const filePath = path.join(OUTPUT_DIR, safeName);
// Check if file exists
if (!fs.existsSync(filePath)) {
return res.status(404).json({
error: 'File not found',
message: `File '${safeName}' does not exist`
});
}
// Send file with proper headers
res.download(filePath, safeName, (err) => {
if (err) {
console.error(`[Public Download] Error: ${err.message}`);
if (!res.headersSent) {
res.status(500).json({
error: 'Download failed',
message: err.message
});
}
}
});
} catch (error) {
console.error(`[Public Download] Error: ${error.message}`);
res.status(500).json({
error: 'Server error',
message: error.message
});
}
});
/**
* GET /info?url=<youtube_url>
* Get info about a video or playlist
@ -192,7 +321,7 @@ app.get('/info', async (req, res) => {
videoCount: info._type === 'playlist' ? info.entries?.length : 1,
});
} catch (error) {
res.status(500).json({ error: error.message });
handleYouTubeError(error, res);
}
});
@ -313,7 +442,12 @@ app.get('/download-stream', async (req, res) => {
});
} catch (error) {
sendEvent('error', { message: error.message });
// Enhanced error for bot detection
if (error.isEnhanced && error.details) {
sendEvent('error', error.details);
} else {
sendEvent('error', { message: error.message });
}
} finally {
res.end();
}
@ -351,7 +485,7 @@ app.post('/download', async (req, res) => {
})),
});
} catch (error) {
res.status(500).json({ error: error.message });
handleYouTubeError(error, res);
}
});
@ -456,6 +590,166 @@ app.post('/upload-transcribe', upload.array('files', 50), async (req, res) => {
}
});
/**
* POST /upload-process
* Smart endpoint that auto-detects input type and processes accordingly:
* - Video files (MP4, AVI, etc.) -> Convert to MP3 -> Transcribe
* - Audio files (MP3, WAV, etc.) -> Transcribe directly
* - URL parameter -> Download from YouTube -> Transcribe
* Body: { url?: string, language?: string, model?: string, outputPath?: string }
* Files: files[] (optional, video or audio files)
*/
app.post('/upload-process', uploadVideo.array('files', 50), async (req, res) => {
try {
if (!process.env.OPENAI_API_KEY) {
return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
}
const { url, language, model = 'gpt-4o-mini-transcribe', outputPath } = req.body;
const outputDir = outputPath || OUTPUT_DIR;
// Detect input type
const hasFiles = req.files && req.files.length > 0;
const hasUrl = url && url.trim() !== '';
if (!hasFiles && !hasUrl) {
return res.status(400).json({
error: 'No input provided. Please provide either files to upload or a URL parameter'
});
}
const results = [];
let totalFiles = 0;
// Process URL if provided
if (hasUrl) {
console.log(`[Smart Process] Detected URL: ${url}`);
try {
// Step 1: Download from YouTube
console.log(`[Smart Process] Downloading from YouTube...`);
const downloadResult = await download(url, { outputDir });
// Step 2: Transcribe downloaded files
const successfulDownloads = downloadResult.videos.filter(v => v.success);
console.log(`[Smart Process] Transcribing ${successfulDownloads.length} downloaded files...`);
for (const video of successfulDownloads) {
totalFiles++;
try {
const transcribeResult = await transcribeAndSave(video.filePath, {
language: language || undefined,
responseFormat: 'text',
outputFormat: 'txt',
model,
outputDir,
});
results.push({
success: true,
source: 'url',
sourceType: 'youtube',
title: video.title,
audioPath: video.filePath,
audioUrl: `/files/${path.basename(video.filePath)}`,
transcriptionPath: transcribeResult.transcriptionPath,
transcriptionUrl: `/files/${path.basename(transcribeResult.transcriptionPath)}`,
text: transcribeResult.text,
});
} catch (error) {
results.push({
success: false,
source: 'url',
title: video.title,
error: error.message,
});
}
}
} catch (error) {
results.push({
success: false,
source: 'url',
error: error.message,
});
}
}
// Process uploaded files if provided
if (hasFiles) {
console.log(`[Smart Process] Detected ${req.files.length} uploaded files`);
for (let i = 0; i < req.files.length; i++) {
const file = req.files[i];
totalFiles++;
const isVideo = /\.(mp4|avi|mkv|mov|wmv|flv|webm|m4v)$/i.test(file.originalname);
const isAudio = /\.(mp3|wav|m4a|flac|ogg|aac)$/i.test(file.originalname);
console.log(`[${i + 1}/${req.files.length}] Processing: ${file.originalname} (${isVideo ? 'video' : 'audio'})`);
try {
let audioFilePath = file.path;
let conversionResult = null;
// Step 1: Convert to MP3 if it's a video
if (isVideo) {
console.log(` → Converting video to MP3...`);
conversionResult = await convertToMP3(file.path, {
outputDir,
bitrate: '192k',
quality: '2',
});
audioFilePath = conversionResult.outputPath;
}
// Step 2: Transcribe the audio
console.log(` → Transcribing audio...`);
const transcribeResult = await transcribeAndSave(audioFilePath, {
language: language || undefined,
responseFormat: 'text',
outputFormat: 'txt',
model,
outputDir,
});
results.push({
success: true,
source: 'upload',
sourceType: isVideo ? 'video' : 'audio',
fileName: file.originalname,
converted: isVideo,
audioPath: audioFilePath,
audioUrl: `/files/${path.basename(audioFilePath)}`,
transcriptionPath: transcribeResult.transcriptionPath,
transcriptionUrl: `/files/${path.basename(transcribeResult.transcriptionPath)}`,
text: transcribeResult.text,
});
} catch (error) {
console.error(` ✗ Failed to process ${file.originalname}: ${error.message}`);
results.push({
success: false,
source: 'upload',
fileName: file.originalname,
error: error.message,
});
}
}
}
const successCount = results.filter(r => r.success).length;
const failCount = results.filter(r => !r.success).length;
res.json({
success: true,
totalFiles,
successCount,
failCount,
results,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
/**
* POST /convert-to-mp3
* Upload video/audio files and convert them to MP3
@ -686,7 +980,12 @@ app.get('/process-stream', async (req, res) => {
});
} catch (error) {
sendEvent('error', { message: error.message });
// Enhanced error for bot detection
if (error.isEnhanced && error.details) {
sendEvent('error', error.details);
} else {
sendEvent('error', { message: error.message });
}
} finally {
res.end();
}
@ -757,7 +1056,7 @@ app.post('/process', async (req, res) => {
results: combinedResults,
});
} catch (error) {
res.status(500).json({ error: error.message });
handleYouTubeError(error, res);
}
});
@ -1163,23 +1462,105 @@ app.get('/summarize-stream', async (req, res) => {
} catch (error) {
console.error(`[Summarize Pipeline] Error: ${error.message}`);
sendEvent('error', { message: error.message });
// Enhanced error for bot detection
if (error.isEnhanced && error.details) {
sendEvent('error', error.details);
} else {
sendEvent('error', { message: error.message });
}
} finally {
res.end();
}
});
/**
* POST /admin/upload-cookies
* Upload YouTube cookies file to persist authentication
*/
const uploadCookies = multer({
storage: multer.memoryStorage(),
fileFilter: (req, file, cb) => {
if (file.mimetype === 'text/plain' || file.originalname.endsWith('.txt')) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only .txt files are allowed.'));
}
},
limits: {
fileSize: 1024 * 1024, // 1MB max
}
});
app.post('/admin/upload-cookies', uploadCookies.single('cookies'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
error: 'No file uploaded',
message: 'Please upload a cookies.txt file',
help: 'Export cookies from your browser using a "Get cookies.txt" extension'
});
}
// Paths for storing cookies
const localCookiesPath = path.join(process.cwd(), 'youtube-cookies.txt');
const shareCookiesPath = '/tmp/share/youtube-cookies.txt';
// Write to local directory
fs.writeFileSync(localCookiesPath, req.file.buffer);
fs.chmodSync(localCookiesPath, 0o600); // Secure permissions
console.log(`✓ Cookies saved to: ${localCookiesPath}`);
// Also save to /tmp/share if it exists (for persistence across restarts)
try {
if (!fs.existsSync('/tmp/share')) {
fs.mkdirSync('/tmp/share', { recursive: true });
}
fs.writeFileSync(shareCookiesPath, req.file.buffer);
fs.chmodSync(shareCookiesPath, 0o600);
console.log(`✓ Cookies also saved to: ${shareCookiesPath} (persistent)`);
} catch (err) {
console.warn(`⚠️ Could not save to /tmp/share: ${err.message}`);
}
// Update environment variable for immediate use
process.env.YOUTUBE_COOKIES_PATH = localCookiesPath;
res.json({
success: true,
message: 'Cookies uploaded successfully',
paths: {
local: localCookiesPath,
persistent: fs.existsSync(shareCookiesPath) ? shareCookiesPath : null,
},
note: 'Cookies are now active. No restart required.'
});
} catch (error) {
console.error(`[Upload Cookies] Error: ${error.message}`);
res.status(500).json({
error: 'Failed to upload cookies',
message: error.message
});
}
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log('\nEndpoints:');
console.log(' GET /health - Health check');
console.log(' GET /docs/api - API documentation (no auth)');
console.log(' GET /public/download/:filename - Public file download (no auth)');
console.log(' GET /public/scripts/extract-and-upload-cookies.sh - Cookie script (no auth)');
console.log(' GET /info?url= - Get video/playlist info');
console.log(' POST /download - Download as MP3');
console.log(' POST /transcribe - Transcribe audio file');
console.log(' POST /process - Download + transcribe');
console.log(' POST /upload-process - Smart: Upload video/audio OR URL -> Transcribe');
console.log(' POST /summarize - Summarize text (GPT-5.1)');
console.log(' POST /summarize-file - Summarize files (GPT-5.1)');
console.log(' GET /summarize-stream - Full pipeline: Download + Transcribe + Summarize');
console.log(' GET /files-list - List downloaded files');
console.log(' GET /files/<name> - Serve downloaded files');
console.log(' POST /admin/upload-cookies - Upload YouTube cookies for bot detection bypass');
});

View File

@ -9,14 +9,75 @@ const YTDLP_PATH = process.env.YTDLP_PATH || 'yt-dlp';
// Path to cookies file (optional)
const COOKIES_PATH = process.env.YOUTUBE_COOKIES_PATH || null;
// Browser to extract cookies from (chrome, firefox, edge, safari, etc.)
const COOKIES_BROWSER = process.env.YOUTUBE_COOKIES_BROWSER || null;
/**
* Add cookies argument if cookies file exists
* Enhanced error message for YouTube bot detection
*/
function enhanceYouTubeError(error) {
const errorMsg = error.message || error.toString();
// Check if it's a bot detection error
if (errorMsg.includes('Sign in to confirm') ||
errorMsg.includes('not a bot') ||
errorMsg.includes('confirm you\'re not a bot') ||
errorMsg.includes('ERROR: Unable to extract')) {
const cookiesConfigured = COOKIES_BROWSER || COOKIES_PATH;
return {
error: 'YouTube Bot Detection',
message: 'YouTube is blocking this request. Authentication required.',
reason: errorMsg,
solution: {
quick: 'Upload fresh cookies from your browser',
steps: [
'1. Install browser extension: "Get cookies.txt LOCALLY"',
'2. Visit youtube.com and log into your account',
'3. Export cookies using the extension',
'4. Upload via API: POST /admin/upload-cookies',
' Or use the web interface at http://yourserver:8888',
],
alternative: 'Use extract-and-upload-cookies.sh script for automation',
documentation: 'See COOKIES_QUICK_START.md for detailed instructions'
},
currentConfig: {
cookiesFile: COOKIES_PATH || 'Not configured',
cookiesBrowser: COOKIES_BROWSER || 'Not configured',
status: cookiesConfigured ? '⚠️ Configured but may be expired' : '❌ Not configured'
}
};
}
// Generic YouTube error
return {
error: 'YouTube Download Failed',
message: errorMsg,
solution: 'Check if the URL is valid and the video is available'
};
}
/**
* Add cookies argument - prioritizes live browser cookies over file
*/
function addCookiesArg(args, cookiesPath = null) {
const cookies = cookiesPath || COOKIES_PATH;
// Option 1: Extract cookies from browser (always fresh)
if (COOKIES_BROWSER) {
console.log(`Using live cookies from ${COOKIES_BROWSER} browser`);
return ['--cookies-from-browser', COOKIES_BROWSER, ...args];
}
// Option 2: Use static cookies file (may expire)
// Check dynamically in case cookies were uploaded after server started
const cookies = cookiesPath || process.env.YOUTUBE_COOKIES_PATH || COOKIES_PATH;
if (cookies && fs.existsSync(cookies)) {
console.log(`Using cookies file: ${cookies}`);
return ['--cookies', cookies, ...args];
}
// Option 3: No cookies (may fail on some videos)
console.log('No cookies configured - some videos may fail');
return args;
}
@ -140,7 +201,11 @@ export async function getInfo(url, forcePlaylist = false, options = {}) {
], options);
return info;
} catch (error) {
throw new Error(`Failed to get info: ${error.message}`);
const enhancedError = enhanceYouTubeError(error);
const err = new Error(JSON.stringify(enhancedError));
err.isEnhanced = true;
err.details = enhancedError;
throw err;
}
}
@ -199,7 +264,11 @@ export async function downloadVideo(url, options = {}) {
url: url,
};
} catch (error) {
throw new Error(`Failed to download: ${error.message}`);
const enhancedError = enhanceYouTubeError(error);
const err = new Error(JSON.stringify(enhancedError));
err.isEnhanced = true;
err.details = enhancedError;
throw err;
}
}
@ -281,7 +350,11 @@ export async function downloadPlaylist(url, options = {}) {
failCount: results.filter(r => !r.success).length,
};
} catch (error) {
throw new Error(`Failed to download playlist: ${error.message}`);
const enhancedError = enhanceYouTubeError(error);
const err = new Error(JSON.stringify(enhancedError));
err.isEnhanced = true;
err.details = enhancedError;
throw err;
}
}