dptxa-proxy / src /pages /api /proxy-media.js
TXAVLOG
Deploy DPTXA to Hugging Face Spaces
4bea261
Raw
History Blame Contribute Delete
5.87 kB
export const OPTIONS = async () => {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Range',
'Access-Control-Max-Age': '86400',
},
});
};
export const GET = async ({ request, url }) => {
const targetUrl = url.searchParams.get('url');
if (!targetUrl) {
return new Response(JSON.stringify({ error: 'Missing target URL' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
try {
const headers = new Headers();
// Use the origin of the target URL as the referer to bypass simple checks
const targetOrigin = new URL(targetUrl).origin;
headers.set('Referer', targetOrigin + '/');
headers.set('Origin', targetOrigin);
headers.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');
// Forward Range header for smooth scrubbing/seeking (HTTP 206 Partial Content support)
const rangeHeader = request.headers.get('Range') || request.headers.get('range');
if (rangeHeader) {
headers.set('Range', rangeHeader);
}
const response = await fetch(targetUrl, { headers });
if (!response.ok) {
// If it's an image and it's not found, return a transparent 1x1 pixel
// This prevents the player from retrying and avoids broken image icons in the UI
const isImage = targetUrl.match(/\.(jpg|jpeg|png|gif|webp|svg)(\?.*)?$/i) ||
targetUrl.toLowerCase().includes('preview.jpg') ||
targetUrl.toLowerCase().includes('thumb.jpg');
if (isImage && response.status === 404) {
// 1x1 transparent GIF
const transparentPixel = Buffer.from('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64');
return new Response(transparentPixel, {
status: 200,
headers: {
'Content-Type': 'image/gif',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=86400'
}
});
}
console.warn(`[Proxy] Failed to fetch ${targetUrl}: ${response.status} ${response.statusText}`);
return new Response(null, { status: response.status });
}
const contentType = response.headers.get('Content-Type') || '';
// If it's a playlist (m3u8), we might need to rewrite relative paths to absolute ones
// so that the player doesn't try to load them relative to the proxy URL.
if (contentType.includes('mpegurl') || contentType.includes('x-mpegURL') || targetUrl.endsWith('.m3u8')) {
let text = await response.text();
const urlObj = new URL(targetUrl);
const baseUrl = targetUrl.substring(0, targetUrl.lastIndexOf('/') + 1);
// Rewrite relative paths to absolute URLs pointing back to the proxy
// This ensures that all segments also go through the proxy to bypass CORS
const proxiedText = text.split('\n').map(line => {
const trimmed = line.trim();
if (!trimmed) return line;
// If it is a tag/comment line
if (trimmed.startsWith('#')) {
// Check for URI attribute in EXT tags (like EXT-X-KEY, EXT-X-MAP, EXT-X-MEDIA)
if (trimmed.includes('URI=')) {
return trimmed.replace(/URI=(["'])([^"'\r\n]+)\1/g, (match, quote, p1) => {
let absolutePath = p1;
if (!p1.startsWith('http')) {
absolutePath = p1.startsWith('/')
? urlObj.origin + p1
: baseUrl + p1;
}
return `URI=${quote}/api/proxy-media?url=${encodeURIComponent(absolutePath)}${quote}`;
});
}
return line;
}
// If it is a segment URL (non-tag line)
// If it's already an absolute URL, wrap it in proxy
if (trimmed.startsWith('http')) {
return `/api/proxy-media?url=${encodeURIComponent(trimmed)}`;
}
// If it's a relative path, make it absolute and wrap it
const absolutePath = trimmed.startsWith('/')
? urlObj.origin + trimmed
: baseUrl + trimmed;
return `/api/proxy-media?url=${encodeURIComponent(absolutePath)}`;
}).join('\n');
return new Response(proxiedText, {
status: 200,
headers: {
'Content-Type': 'application/x-mpegURL',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'no-cache'
}
});
}
// For other media (images, segments), stream the response directly to the browser
// This achieves zero-latency start and supports seeking (206 Partial Content)
const status = response.status;
const responseHeaders = new Headers({
'Content-Type': contentType,
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=3600'
});
// Copy Range-related headers from destination response if they exist
const contentRange = response.headers.get('Content-Range') || response.headers.get('content-range');
if (contentRange) {
responseHeaders.set('Content-Range', contentRange);
}
const contentLength = response.headers.get('Content-Length') || response.headers.get('content-length');
if (contentLength) {
responseHeaders.set('Content-Length', contentLength);
}
const acceptRanges = response.headers.get('Accept-Ranges') || response.headers.get('accept-ranges');
if (acceptRanges) {
responseHeaders.set('Accept-Ranges', acceptRanges);
}
return new Response(response.body, {
status: status,
headers: responseHeaders
});
} catch (error) {
console.error(`[Proxy] Error:`, error);
return new Response(null, { status: 500 });
}
};