/** * DownTube Cloudflare Worker - YouTube Download Proxy * * Uses YouTube's Innertube API from Cloudflare's Edge Network * Cloudflare Workers run on residential-like IPs that YouTube doesn't block! * * Free tier: 100,000 requests/day * No credit card needed * * Endpoints: * GET / - Health check * POST /info - Get video info (title, thumbnail, formats) * POST /download - Get download URL for a specific quality * POST /stream - Get streaming data (all formats) */ // YouTube Innertube API keys (public, hardcoded in YouTube's frontend) const INNERTUBE_API_KEY = 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w'; const INNERTUBE_API_URL = 'https://www.youtube.com/youtubei/v1/player'; // Client configurations for different YouTube clients const CLIENTS = { android: { clientName: 'ANDROID', clientVersion: '19.29.37', androidSdkVersion: 30, hl: 'en', gl: 'US', }, ios: { clientName: 'IOS', clientVersion: '19.29.1', deviceModel: 'iPhone16,2', iosVersion: '17.5.1', hl: 'en', gl: 'US', }, web: { clientName: 'WEB', clientVersion: '2.20240726.00.00', hl: 'en', gl: 'US', }, mweb: { clientName: 'MWEB', clientVersion: '2.20240726.01.00', hl: 'en', gl: 'US', }, tv_embedded: { clientName: 'TVHTML5_SIMPLY_EMBEDDED_PLAYER', clientVersion: '2.0', hl: 'en', gl: 'US', }, }; // CORS headers const CORS_HEADERS = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', 'Content-Type': 'application/json', }; // Quality preferences mapping const QUALITY_HEIGHT_MAP = { best: 1080, medium: 720, low: 480, }; /** * Extract video ID from various YouTube URL formats */ function extractVideoId(url) { const patterns = [ /(?:v=|\/v\/|youtu\.be\/|\/embed\/|\/shorts\/)([a-zA-Z0-9_-]{11})/, /^([a-zA-Z0-9_-]{11})$/, ]; for (const pattern of patterns) { const match = url.match(pattern); if (match) return match[1]; } return null; } /** * Call YouTube's Innertube Player API */ async function fetchPlayerData(videoId, clientType = 'android') { const client = CLIENTS[clientType]; if (!client) throw new Error(`Unknown client: ${clientType}`); const body = { videoId, context: { client, }, playbackContext: { contentPlaybackContext: { html5Preference: 'HTML5_PREF_WANTS', }, }, contentCheckOk: true, racyCheckOk: true, }; const response = await fetch( `${INNERTUBE_API_URL}?key=${INNERTUBE_API_KEY}&prettyPrint=false`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': 'com.google.android.youtube/19.29.37 (Linux; U; Android 14)', 'Accept': 'application/json', }, body: JSON.stringify(body), } ); if (!response.ok) { throw new Error(`Innertube API returned ${response.status}`); } return await response.json(); } /** * Try multiple clients to get player data */ async function fetchWithFallback(videoId) { const clientOrder = ['android', 'ios', 'mweb', 'web', 'tv_embedded']; let lastError = null; for (const clientType of clientOrder) { try { const data = await fetchPlayerData(videoId, clientType); // Check if video is playable const status = data?.playabilityStatus?.status; if (status === 'OK' && data?.streamingData) { return { data, clientType }; } // If login required, try next client if (status === 'LOGIN_REQUIRED' || status === 'UNPLAYABLE') { const reason = data?.playabilityStatus?.reason || 'Unknown'; console.log(`Client ${clientType}: ${status} - ${reason}`); lastError = new Error(`${status}: ${reason}`); continue; } // If age restricted, try next client if (status === 'AGE_CHECK_REQUIRED') { console.log(`Client ${clientType}: Age check required, trying next`); lastError = new Error('Age restricted video'); continue; } // Has streaming data but different status if (data?.streamingData) { return { data, clientType }; } lastError = new Error(`No streaming data from ${clientType}`); } catch (err) { console.log(`Client ${clientType} failed: ${err.message}`); lastError = err; } } throw lastError || new Error('All clients failed'); } /** * Parse formats from streaming data */ function parseFormats(streamingData) { const formats = []; const adaptiveFormats = []; // Combined formats (video + audio) if (streamingData.formats) { for (const f of streamingData.formats) { formats.push({ itag: f.itag, url: f.url || null, mimeType: f.mimeType || '', quality: f.quality || '', qualityLabel: f.qualityLabel || '', width: f.width || 0, height: f.height || 0, fps: f.fps || 0, bitrate: f.bitrate || 0, contentLength: f.contentLength || 0, hasAudio: true, hasVideo: true, signatureCipher: f.signatureCipher || f.cipher || null, }); } } // Adaptive formats (separate video/audio) if (streamingData.adaptiveFormats) { for (const f of streamingData.adaptiveFormats) { const isVideo = f.mimeType?.startsWith('video/'); const isAudio = f.mimeType?.startsWith('audio/'); adaptiveFormats.push({ itag: f.itag, url: f.url || null, mimeType: f.mimeType || '', quality: f.quality || '', qualityLabel: f.qualityLabel || '', width: f.width || 0, height: f.height || 0, fps: f.fps || 0, bitrate: f.bitrate || 0, contentLength: f.contentLength || 0, audioQuality: f.audioQuality || '', audioSampleRate: f.audioSampleRate || '', audioChannels: f.audioChannels || 0, hasAudio: isAudio, hasVideo: isVideo, signatureCipher: f.signatureCipher || f.cipher || null, }); } } return { formats, adaptiveFormats }; } /** * Find the best download URL for a given quality */ function findBestDownload(parsedFormats, quality = 'best') { const targetHeight = QUALITY_HEIGHT_MAP[quality] || 1080; const { formats, adaptiveFormats } = parsedFormats; // Strategy 1: Find a combined format (video+audio) at target quality const combinedFormats = formats .filter(f => f.url && f.height <= targetHeight) .sort((a, b) => b.height - a.height); if (combinedFormats.length > 0) { return { type: 'combined', videoUrl: combinedFormats[0].url, audioUrl: null, needsMerge: false, format: combinedFormats[0], }; } // Strategy 2: Find separate video and audio streams const videoFormats = adaptiveFormats .filter(f => f.hasVideo && f.url && f.height <= targetHeight) .sort((a, b) => b.height - a.height); const audioFormats = adaptiveFormats .filter(f => f.hasAudio && f.url) .sort((a, b) => b.bitrate - a.bitrate); if (videoFormats.length > 0 && audioFormats.length > 0) { return { type: 'separate', videoUrl: videoFormats[0].url, audioUrl: audioFormats[0].url, needsMerge: true, videoFormat: videoFormats[0], audioFormat: audioFormats[0], }; } // Strategy 3: Any available URL const anyFormat = [...formats, ...adaptiveFormats].find(f => f.url); if (anyFormat) { return { type: 'any', videoUrl: anyFormat.url, audioUrl: null, needsMerge: false, format: anyFormat, }; } return null; } /** * Extract video info from player data */ function extractVideoInfo(playerData) { const videoDetails = playerData.videoDetails || {}; return { videoId: videoDetails.videoId || '', title: videoDetails.title || '', lengthSeconds: parseInt(videoDetails.lengthSeconds || '0'), keywords: videoDetails.keywords || [], channelId: videoDetails.channelId || '', author: videoDetails.author || '', shortDescription: videoDetails.shortDescription || '', viewCount: parseInt(videoDetails.viewCount || '0'), thumbnails: videoDetails.thumbnail?.thumbnails || [], isLiveContent: videoDetails.isLiveContent || false, }; } /** * Handle CORS preflight */ function handleOptions() { return new Response(null, { status: 204, headers: CORS_HEADERS, }); } /** * Main request handler */ export default { async fetch(request, env, ctx) { const url = new URL(request.url); const path = url.pathname; // Handle CORS if (request.method === 'OPTIONS') { return handleOptions(); } // Health check if (path === '/' || path === '/health') { return new Response(JSON.stringify({ status: 'ok', service: 'DownTube Worker', version: '1.0.0', endpoints: ['/info', '/download', '/stream'], }), { headers: CORS_HEADERS }); } // Only accept POST for API endpoints if (request.method !== 'POST') { return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, headers: CORS_HEADERS, }); } try { const body = await request.json(); const videoUrl = body.url || body.video_url || ''; const quality = body.quality || 'best'; if (!videoUrl) { return new Response(JSON.stringify({ error: 'Missing url parameter' }), { status: 400, headers: CORS_HEADERS, }); } const videoId = extractVideoId(videoUrl); if (!videoId) { return new Response(JSON.stringify({ error: 'Invalid YouTube URL' }), { status: 400, headers: CORS_HEADERS, }); } // Fetch player data with fallback clients const { data: playerData, clientType } = await fetchWithFallback(videoId); const videoInfo = extractVideoInfo(playerData); const parsedFormats = parseFormats(playerData.streamingData || {}); // Check for signature cipher (needs further processing) const hasCipher = [...parsedFormats.formats, ...parsedFormats.adaptiveFormats] .some(f => f.signatureCipher); // ─── /info endpoint ─── if (path === '/info') { // Get captions/subtitles const captions = playerData.captions?.playerCaptionsTracklistRenderer?.captionTracks || []; const subtitles = captions.map(cap => ({ languageCode: cap.languageCode || '', name: cap.name?.simpleText || cap.languageCode || '', url: cap.baseUrl || '', })); return new Response(JSON.stringify({ success: true, source: 'cloudflare_worker', clientUsed: clientType, video: videoInfo, subtitles, hasCipher, formatCount: parsedFormats.formats.length, adaptiveFormatCount: parsedFormats.adaptiveFormats.length, }), { headers: CORS_HEADERS }); } // ─── /stream endpoint ─── if (path === '/stream') { return new Response(JSON.stringify({ success: true, source: 'cloudflare_worker', clientUsed: clientType, video: videoInfo, formats: parsedFormats.formats, adaptiveFormats: parsedFormats.adaptiveFormats, hasCipher, expiresInSeconds: playerData.streamingData?.expiresInSeconds || '', }), { headers: CORS_HEADERS }); } // ─── /download endpoint (default) ─── const download = findBestDownload(parsedFormats, quality); if (!download) { // If all URLs have cipher, return stream data for client-side processing if (hasCipher) { return new Response(JSON.stringify({ success: false, error: 'Video requires signature deciphering', source: 'cloudflare_worker', clientUsed: clientType, video: videoInfo, hint: 'Use /stream endpoint to get raw format data', hasCipher: true, }), { status: 422, headers: CORS_HEADERS }); } return new Response(JSON.stringify({ success: false, error: 'No downloadable formats found', source: 'cloudflare_worker', }), { status: 404, headers: CORS_HEADERS }); } // Determine filename const safeTitle = (videoInfo.title || 'video').replace(/[^\w\s-]/g, '').substring(0, 80); const ext = download.needsMerge ? 'mp4' : (download.format?.mimeType?.includes('webm') ? 'webm' : 'mp4'); return new Response(JSON.stringify({ success: true, source: 'cloudflare_worker', clientUsed: clientType, video: videoInfo, download: { type: download.type, needsMerge: download.needsMerge, videoUrl: download.videoUrl, audioUrl: download.audioUrl, filename: `${safeTitle}.${ext}`, format: download.format || download.videoFormat, audioFormat: download.audioFormat || null, }, hasCipher, }), { headers: CORS_HEADERS }); } catch (err) { console.error('Worker error:', err); return new Response(JSON.stringify({ success: false, error: err.message || 'Internal server error', source: 'cloudflare_worker', }), { status: 500, headers: CORS_HEADERS }); } }, };