Z User
v5.0: Cloudflare Worker strategy - permanent free solution
61d3f7c
Raw
History Blame Contribute Delete
13.6 kB
/**
* 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 });
}
},
};