File size: 5,869 Bytes
4bea261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
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 });
  }
};