Spaces:
Sleeping
Sleeping
| 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 }); | |
| } | |
| }; | |