TXAVLOG
update cloudflare worker proxy configuration for new account txasoftdev
1662bc8
Raw
History Blame Contribute Delete
10.9 kB
/**
* TPhimX - Cloudflare Worker HLS Media Proxy & R2 Auto-Cache CDN
*
* Features:
* 1. CORS Bypass: Attaches Access-Control-Allow-Origin: * to all requests.
* 2. Range Requests: Supports seeking/scrubbing in HLS players (206 Partial Content).
* 3. Dynamic R2 Caching: Checks R2 first. On cache miss, fetches original video segment,
* caches it into R2, and pipes it back to the client.
* 4. Playlist Rewriter: Rewrites .m3u8 relative and absolute URLs to route all subsequent
* .ts media segment requests through this Worker and cache them in R2 automatically.
*/
export default {
async fetch(request, env, ctx) {
const urlObj = new URL(request.url);
const targetUrlStr = urlObj.searchParams.get('url');
const hasR2 = env.tphimx_movie !== undefined;
if (!targetUrlStr) {
// Direct R2 Path-based Request: e.g. /fbi-part-one/360p/index.m3u8
const pathname = urlObj.pathname;
if (pathname && pathname !== '/') {
if (!hasR2) {
return new Response('R2 Cache storage is not enabled on this Worker account yet.', {
status: 400,
headers: { 'Access-Control-Allow-Origin': '*' }
});
}
const r2Key = pathname.replace(/^\/+/, ''); // Convert leading slash to relative key
try {
const object = await env.tphimx_movie.get(r2Key);
if (object !== null) {
// Cache HIT - Serve directly from R2
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('Access-Control-Allow-Origin', '*');
headers.set('X-TPhimX-Cache', 'HIT (Direct R2 Path)');
// Handle Range Requests for seekability
const rangeHeader = request.headers.get('Range');
if (rangeHeader && object.size > 0) {
const parts = rangeHeader.replace(/bytes=/, "").split("-");
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : object.size - 1;
if (start >= 0 && end < object.size && start <= end) {
const slicedObject = await env.tphimx_movie.get(r2Key, {
range: { offset: start, length: end - start + 1 }
});
headers.set('Content-Range', `bytes ${start}-${end}/${object.size}`);
headers.set('Content-Length', String(end - start + 1));
return new Response(slicedObject.body, {
status: 206,
headers
});
}
}
return new Response(object.body, {
status: 200,
headers
});
} else {
return new Response('File not found in R2 bucket', {
status: 404,
headers: { 'Access-Control-Allow-Origin': '*' }
});
}
} catch (err) {
return new Response(`R2 Path Error: ${err.message}`, {
status: 500,
headers: { 'Access-Control-Allow-Origin': '*' }
});
}
}
return new Response(JSON.stringify({
error: 'Missing "url" search parameter.',
usage: `${urlObj.origin}/?url=https://example.com/stream.m3u8`
}), {
status: 400,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
// Handle OPTIONS Preflight request
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
'Access-Control-Allow-Headers': '*',
'Access-Control-Max-Age': '86400'
}
});
}
let targetUrl;
try {
targetUrl = new URL(targetUrlStr);
} catch (e) {
return new Response('Invalid target URL format', { status: 400, headers: { 'Access-Control-Allow-Origin': '*' } });
}
// Generate unique storage key for Cloudflare R2
// Scheme: cache/<hostname>/<clean_pathname>
const cleanPath = targetUrl.pathname.replace(/^\/+/, '');
const r2Key = `cache/${targetUrl.hostname}/${cleanPath}`;
// 1. Check if file already exists in Cloudflare R2 bucket (Only check if R2 is enabled)
if (hasR2) {
try {
const object = await env.tphimx_movie.get(r2Key);
if (object !== null) {
// Cache HIT - Serve directly from R2
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('Access-Control-Allow-Origin', '*');
headers.set('X-TPhimX-Cache', 'HIT (Cloudflare R2)');
// Handle Range Requests for seekability
const rangeHeader = request.headers.get('Range');
if (rangeHeader && object.size > 0) {
const parts = rangeHeader.replace(/bytes=/, "").split("-");
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : object.size - 1;
if (start >= 0 && end < object.size && start <= end) {
// Slice the R2 object
const slicedObject = await env.tphimx_movie.get(r2Key, {
range: { offset: start, length: end - start + 1 }
});
headers.set('Content-Range', `bytes ${start}-${end}/${object.size}`);
headers.set('Content-Length', String(end - start + 1));
return new Response(slicedObject.body, {
status: 206,
headers
});
}
}
return new Response(object.body, {
status: 200,
headers
});
}
} catch (err) {
// Log error but fallback to fetching from source to prevent playback interruption
console.error('R2 read error:', err.message);
}
}
// Cache MISS - Fetch from original media source server
try {
// Forward request headers like Range
const fetchHeaders = new Headers();
// Bypass anti-hotlinking/Referer checks of media providers (e.g. s2.phim1280.tv)
const targetOrigin = targetUrl.origin;
fetchHeaders.set('Referer', targetOrigin + '/');
fetchHeaders.set('Origin', targetOrigin);
fetchHeaders.set('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
const range = request.headers.get('Range');
if (range) fetchHeaders.set('Range', range);
const response = await fetch(targetUrlStr, {
headers: fetchHeaders
});
if (!response.ok && response.status !== 206) {
const responseText = await response.text().catch(() => '');
return new Response(JSON.stringify({
error: `Failed to fetch original media source (HTTP ${response.status})`,
targetUrl: targetUrlStr,
sentHeaders: Object.fromEntries(fetchHeaders.entries()),
responseHeaders: Object.fromEntries(response.headers.entries()),
responseBody: responseText.substring(0, 500)
}), {
status: response.status,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
const contentType = response.headers.get('Content-Type') || '';
const isM3U8 = targetUrl.pathname.endsWith('.m3u8') ||
contentType.includes('mpegurl') ||
contentType.includes('application/x-mpegURL');
if (isM3U8) {
// Rewrite HLS playlist URLs to route through this Worker Proxy
let playlistText = await response.text();
const baseTargetUrl = targetUrl.origin + targetUrl.pathname.substring(0, targetUrl.pathname.lastIndexOf('/') + 1);
const lines = playlistText.split('\n');
const rewrittenLines = lines.map(line => {
const trimmed = line.trim();
if (trimmed.length === 0 || trimmed.startsWith('#')) {
return line;
}
// Resolve absolute URL
let absoluteUrl = trimmed;
if (!trimmed.startsWith('http://') && !trimmed.startsWith('https://')) {
if (trimmed.startsWith('/')) {
absoluteUrl = targetUrl.origin + trimmed;
} else {
absoluteUrl = baseTargetUrl + trimmed;
}
}
// Rewritten relative segment URL to pipe through the worker
return `${urlObj.origin}/?url=${encodeURIComponent(absoluteUrl)}`;
});
const rewrittenPlaylist = rewrittenLines.join('\n');
const playlistBuffer = new TextEncoder().encode(rewrittenPlaylist);
// Save the rewritten playlist into Cloudflare R2 bucket (only if R2 is enabled)
if (hasR2) {
ctx.waitUntil(
env.tphimx_movie.put(r2Key, playlistBuffer, {
httpMetadata: { contentType: 'application/x-mpegURL' }
}).catch(e => console.error('R2 Put Error:', e.message))
);
}
return new Response(rewrittenPlaylist, {
status: 200,
headers: {
'Content-Type': 'application/x-mpegURL',
'Access-Control-Allow-Origin': '*',
'X-TPhimX-Cache': hasR2 ? 'MISS (Rewritten & Cached)' : 'MISS (Rewritten)'
}
});
} else {
// Fetch raw binary segment file (.ts)
const segmentData = await response.arrayBuffer();
// Save the segment file into Cloudflare R2 bucket asynchronously (only if R2 is enabled)
if (hasR2) {
ctx.waitUntil(
env.tphimx_movie.put(r2Key, segmentData, {
httpMetadata: { contentType: contentType }
}).catch(e => console.error('R2 Put Error:', e.message))
);
}
// Build Response
const headers = new Headers();
headers.set('Content-Type', contentType);
headers.set('Access-Control-Allow-Origin', '*');
headers.set('X-TPhimX-Cache', 'MISS (Cached Segment)');
if (response.status === 206) {
const contentRange = response.headers.get('Content-Range');
if (contentRange) headers.set('Content-Range', contentRange);
return new Response(segmentData, {
status: 206,
headers
});
}
return new Response(segmentData, {
status: 200,
headers
});
}
} catch (fetchErr) {
return new Response(`Proxy error: ${fetchErr.message}`, {
status: 502,
headers: { 'Access-Control-Allow-Origin': '*' }
});
}
}
};