diff --git a/.env_example b/.env_example index 8d239c3..f7194c4 100644 --- a/.env_example +++ b/.env_example @@ -1,2 +1,3 @@ TMDB_API_KEY="your key here" # Replace with your TMDB API key. You can get one at https://www.themoviedb.org/settings/api -PORT="3000" # The port on which the server will run. You can change this to any available port. default is 3000 \ No newline at end of file +PORT="3000" # The port on which the server will run. You can change this to any available port. default is 3000 +ALLOWED_ORIGINS=[] # Add allowed origins here. Example ["http://localhost:3000", "https://example.com"] \ No newline at end of file diff --git a/index.js b/index.js index 7661ed5..a716365 100644 --- a/index.js +++ b/index.js @@ -17,7 +17,7 @@ import { startup } from './src/utils/startup.js'; import { fileURLToPath } from 'url'; const PORT = process.env.PORT; -const allowedOrigins = ['https://cinepro.mintlify.app/']; // localhost is also allowed. (from any localhost port) +const allowedOrigins = process.env.ALLOWED_ORIGINS; // localhost is also allowed. (from any localhost port) const app = express(); app.use( @@ -150,7 +150,7 @@ app.get('/cache-stats', (req, res) => { }); }); -app.get('*', (req, res) => { +app.get('/{*any}', (req, res) => { handleErrorResponse( res, new ErrorObject( diff --git a/src/proxy/proxyserver.js b/src/proxy/proxyserver.js index be9ba23..1b218ee 100644 --- a/src/proxy/proxyserver.js +++ b/src/proxy/proxyserver.js @@ -40,77 +40,20 @@ function cleanupCache() { // Start cleanup interval setInterval(cleanupCache, 30 * 60 * 1000); // every 30 minutes -function getCachedSegment(url) { - if (isCacheDisabled()) return undefined; - - const entry = segmentCache.get(url); - if (entry) { - if (Date.now() - entry.timestamp > CACHE_EXPIRY_MS) { - segmentCache.delete(url); - return undefined; - } - return entry; - } - return undefined; -} - -async function prefetchSegment(url, headers) { - if (isCacheDisabled() || segmentCache.size >= CACHE_MAX_SIZE) { - return; - } - - const existing = segmentCache.get(url); - const now = Date.now(); - if (existing && now - existing.timestamp <= CACHE_EXPIRY_MS) { - return; // already cached and fresh - } - - try { - const response = await fetch(url, { - method: 'GET', - headers: { - 'User-Agent': DEFAULT_USER_AGENT, - ...headers - } - }); - - if (!response.ok) { - // failed to prefetch - return; - } - - const data = new Uint8Array(await response.arrayBuffer()); - - const responseHeaders = {}; - response.headers.forEach((value, key) => { - responseHeaders[key] = value; - }); - - segmentCache.set(url, { - data, - headers: responseHeaders, - timestamp: Date.now() - }); - } catch (error) { - if (process.argv.includes('--debug')) { - console.log(`Prefetch error: ${error.message}`); - } - } -} - -// defaultt user agent i think adding the user agent in the url it self wil mess things up +// Default user agent const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'; function getOriginFromUrl(url) { try { - let origin = new URL(url).origin; + const urlObj = new URL(url); + const origin = urlObj.origin; if (origin.includes(VIDSRC_HLS_ORIGIN)) { return undefined; } - return; + return origin; } catch { - return url; + return undefined; } } @@ -160,436 +103,296 @@ function extractOriginalUrl(proxyUrl) { } } -export function createProxyRoutes(app) { - // M3U8 Proxy endpoint - app.get('/m3u8-proxy', cors(), async (req, res) => { - const targetUrl = req.query.url; - let headers = {}; +// Enhanced CORS middleware based on the working implementation +function handleCors(req, res) { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Range, Accept, Origin, X-Requested-With'); + res.setHeader('Access-Control-Max-Age', '86400'); + + if (req.method === 'OPTIONS') { + res.writeHead(204); + res.end(); + return true; + } + return false; +} - try { - headers = JSON.parse(req.query.headers || '{}'); - } catch (e) { - if (process.argv.includes('--debug')) { - console.log('Invalid headers JSON'); +// M3U8 proxy function based on the working implementation +async function proxyM3U8(targetUrl, headers, res, serverUrl) { + try { + const response = await fetch(targetUrl, { + headers: { + 'User-Agent': DEFAULT_USER_AGENT, + ...headers } - } + }); - if (!targetUrl) { - return res.status(400).json({ error: 'URL parameter required' }); + if (!response.ok) { + res.writeHead(response.status); + res.end(`M3U8 fetch failed: ${response.status}`); + return; } - try { - const response = await fetch(targetUrl, { - headers: { - 'User-Agent': DEFAULT_USER_AGENT, - ...headers + const m3u8Content = await response.text(); + + // Process M3U8 content line by line - key difference from our previous implementation + const processedLines = m3u8Content + .split('\n') + .map(line => { + line = line.trim(); + + // Skip empty lines and comments (except special ones) + if (!line || (line.startsWith('#') && !line.includes('URI='))) { + return line; } - }); - if (process.argv.includes('--debug')) { - console.log( - `[M3U8] Response: ${response.status} ${response.statusText}` - ); - console.log('[M3U8] Request Headers', headers); - console.log('[M3U8] Response Headers'); - response.headers.forEach((v, k) => - console.log(` ${k}: ${v}`) - ); - } - if (!response.ok) { - return res.status(response.status).json({ - error: `M3U8 fetch failed: ${response.status}` - }); - } + // Handle URI in #EXT-X-MEDIA tags (for audio/subtitle tracks) + if (line.startsWith('#EXT-X-MEDIA:') && line.includes('URI=')) { + const uriMatch = line.match(/URI="([^"]+)"/); + if (uriMatch) { + const mediaUrl = new URL(uriMatch[1], targetUrl).href; + const proxyUrl = `${serverUrl}/m3u8-proxy?url=${encodeURIComponent(mediaUrl)}`; + return line.replace(uriMatch[1], proxyUrl); + } + return line; + } - let m3u8Content = await response.text(); - const lines = m3u8Content.split('\n'); - const newLines = []; - const segmentUrls = []; - - // Get base URL for proxying - const protocol = req.headers['x-forwarded-proto'] || 'http'; - const host = req.headers.host; - const baseProxyUrl = `${protocol}://${host}`; - - for (const line of lines) { - if (line.startsWith('#')) { - if (line.startsWith('#EXT-X-KEY:')) { - // Handle encryption keys - const regex = /https?:\/\/[^""\s]+/g; - const keyUrl = regex.exec(line)?.[0]; - if (keyUrl) { - const proxyUrl = `${baseProxyUrl}/ts-proxy?url=${encodeURIComponent(keyUrl)}&headers=${encodeURIComponent(JSON.stringify(headers))}`; - newLines.push(line.replace(keyUrl, proxyUrl)); - - if (!isCacheDisabled()) { - prefetchSegment(keyUrl, headers); - } - } else { - newLines.push(line); - } - } else if ( - line.startsWith('#EXT-X-MEDIA:') || - line.startsWith('#EXT-X-I-FRAME-STREAM-INF:') - ) { - // Handle audio tracks, subtitle tracks, and i-frame streams - const uriMatch = line.match(/URI="([^"]+)"/); - if (uriMatch) { - let mediaUrl = uriMatch[1]; - try { - // Resolve relative URLs - mediaUrl = new URL(mediaUrl, targetUrl).href; - const proxyUrl = `${baseProxyUrl}/m3u8-proxy?url=${encodeURIComponent(mediaUrl)}&headers=${encodeURIComponent(JSON.stringify(headers))}`; - newLines.push( - line.replace(uriMatch[1], proxyUrl) - ); - } catch { - newLines.push(line); // Keep original if URL parsing fails - } - } else { - newLines.push(line); - } - } else { - newLines.push(line); + // Handle encryption keys + if (line.startsWith('#EXT-X-KEY:') && line.includes('URI=')) { + const uriMatch = line.match(/URI="([^"]+)"/); + if (uriMatch) { + const keyUrl = new URL(uriMatch[1], targetUrl).href; + const proxyUrl = `${serverUrl}/ts-proxy?url=${encodeURIComponent(keyUrl)}`; + return line.replace(uriMatch[1], proxyUrl); } - } else if (line.trim() && !line.startsWith('#')) { + return line; + } + + // Handle segment URLs (non-comment lines) + if (!line.startsWith('#')) { try { const segmentUrl = new URL(line, targetUrl).href; - - // Check if this is an m3u8 file (variant playlist) or TS segment - if (/\.m3u8(\?|$)/i.test(segmentUrl)) { - // This is a variant playlist, route to m3u8-proxy - const proxyUrl = `${baseProxyUrl}/m3u8-proxy?url=${encodeURIComponent(segmentUrl)}&headers=${encodeURIComponent(JSON.stringify(headers))}`; - newLines.push(proxyUrl); - } else if (/\.ts(\?|$)/i.test(segmentUrl)) { - // This is a TS segment, route to ts-proxy - segmentUrls.push(segmentUrl); - const proxyUrl = `${baseProxyUrl}/ts-proxy?url=${encodeURIComponent(segmentUrl)}&headers=${encodeURIComponent(JSON.stringify(headers))}`; - newLines.push(proxyUrl); + + // Check if it's another m3u8 file (master playlist) + if (line.includes('.m3u8') || line.includes('m3u8')) { + return `${serverUrl}/m3u8-proxy?url=${encodeURIComponent(segmentUrl)}`; + } else { + // It's a media segment + return `${serverUrl}/ts-proxy?url=${encodeURIComponent(segmentUrl)}`; } - } catch { - newLines.push(line); + } catch (e) { + return line; // Return original if URL parsing fails } - } else { - newLines.push(line); } - } - // Prefetch segments if cache enabled - if (segmentUrls.length > 0 && !isCacheDisabled()) { - cleanupCache(); + return line; + }); - // Prefetch in background, don't wait - Promise.all( - segmentUrls.map((url) => prefetchSegment(url, headers)) - ).catch((err) => { - if (process.argv.includes('--debug')) { - console.log('Prefetch error:', err.message); - } - }); - } + const processedContent = processedLines.join('\n'); - // Set proper headers - res.setHeader('Content-Type', 'application/vnd.apple.mpegurl'); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Headers', '*'); - res.setHeader('Access-Control-Allow-Methods', '*'); - res.setHeader( - 'Cache-Control', - 'no-cache, no-store, must-revalidate' - ); - // Set filename - res.setHeader( - 'Content-Disposition', - `attachment; filename="master.m3u8"` - ); - - res.send(newLines.join('\n')); - } catch (error) { - res.status(500).json( - new ErrorObject( - `M3U8 Proxy unexpected error: ${error.message}`, - 'M3U8 Proxy', - 500, - 'Check implementation or site status', - true, - true - ).toJSON() - ); + // Set proper headers + res.setHeader('Content-Type', 'application/vnd.apple.mpegurl'); + res.setHeader('Content-Length', Buffer.byteLength(processedContent)); + res.setHeader('Cache-Control', 'no-cache'); + + res.writeHead(200); + res.end(processedContent); + + } catch (error) { + console.error('[M3U8 Proxy Error]:', error.message); + res.writeHead(500); + res.end(`M3U8 Proxy error: ${error.message}`); + } +} + +// TS/Segment proxy function based on the working implementation +async function proxyTs(targetUrl, headers, req, res) { + try { + // Handle range requests for video playback + const fetchHeaders = { + 'User-Agent': DEFAULT_USER_AGENT, + ...headers + }; + + // Forward range header if present + if (req.headers.range) { + fetchHeaders['Range'] = req.headers.range; } - }); - // TS/Segment Proxy endpoint - app.get('/ts-proxy', cors(), async (req, res) => { + const response = await fetch(targetUrl, { + headers: fetchHeaders + }); + + if (!response.ok) { + res.writeHead(response.status); + res.end(`TS fetch failed: ${response.status}`); + return; + } + + // Set response headers + const contentType = response.headers.get('content-type') || 'video/mp2t'; + res.setHeader('Content-Type', contentType); + + // Forward important headers from upstream + if (response.headers.get('content-length')) { + res.setHeader('Content-Length', response.headers.get('content-length')); + } + if (response.headers.get('content-range')) { + res.setHeader('Content-Range', response.headers.get('content-range')); + } + if (response.headers.get('accept-ranges')) { + res.setHeader('Accept-Ranges', response.headers.get('accept-ranges')); + } + + // Set status code for range requests + if (response.status === 206) { + res.writeHead(206); + } else { + res.writeHead(200); + } + + // Stream the response directly + response.body.pipe(res); + + } catch (error) { + console.error('[TS Proxy Error]:', error.message); + res.writeHead(500); + res.end(`TS Proxy error: ${error.message}`); + } +} + +export function createProxyRoutes(app) { + // Test endpoint to verify proxy is working + app.get('/proxy/test', (req, res) => { + if (handleCors(req, res)) return; + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + status: 'Proxy server is working', + timestamp: new Date().toISOString(), + userAgent: req.headers['user-agent'] + })); + }); + + // Simplified M3U8 Proxy endpoint based on working implementation + app.get('/m3u8-proxy', (req, res) => { + if (handleCors(req, res)) return; + const targetUrl = req.query.url; let headers = {}; try { headers = JSON.parse(req.query.headers || '{}'); } catch (e) { - console.log( - 'Invalid headers JSON for TS proxy:', - req.query.headers - ); + // Invalid headers JSON } if (!targetUrl) { - return res.status(400).json({ error: 'URL parameter required' }); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'URL parameter required' })); + return; } - try { - // Check cache first if enabled - if (!isCacheDisabled()) { - const cachedSegment = getCachedSegment(targetUrl); - - if (cachedSegment) { - console.log(`[TS Cache Hit] ${targetUrl}`); - - res.setHeader( - 'Content-Type', - cachedSegment.headers['content-type'] || 'video/mp2t' - ); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Headers', '*'); - res.setHeader('Access-Control-Allow-Methods', '*'); - res.setHeader('Cache-Control', 'public, max-age=3600'); - - return res.send(Buffer.from(cachedSegment.data)); - } - } - - console.log(`[TS] Fetching: ${targetUrl}`); + // Get server URL for building proxy URLs + const protocol = req.headers['x-forwarded-proto'] || req.protocol || 'http'; + const host = req.headers.host; + const serverUrl = `${protocol}://${host}`; - const response = await fetch(targetUrl, { - headers: { - 'User-Agent': DEFAULT_USER_AGENT, - ...headers - } - }); - console.log( - `[TS Proxy] Response: ${response.status} ${response.statusText}` - ); - console.log(`[TS Proxy] Request Headers:`, headers); - console.log(`[TS Proxy] Response Headers:`); - response.headers.forEach((v, k) => console.log(` ${k}: ${v}`)); + proxyM3U8(targetUrl, headers, res, serverUrl); + }); - if (!response.ok) { - console.log( - `[TS Proxy] Error fetching segment ${targetUrl} → ${response.status}` - ); - return res.status(response.status).json({ - error: `TS fetch failed: ${response.status}` - }); - } + // Simplified TS/Segment Proxy endpoint + app.get('/ts-proxy', (req, res) => { + if (handleCors(req, res)) return; + + const targetUrl = req.query.url; + let headers = {}; - // Set headers for segment - res.setHeader('Content-Type', 'video/mp2t'); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Headers', '*'); - res.setHeader('Access-Control-Allow-Methods', '*'); - res.setHeader('Cache-Control', 'public, max-age=3600'); - // set filename for TS segments - res.setHeader( - 'Content-Disposition', - `attachment; filename="media.mp4"` - ); + try { + headers = JSON.parse(req.query.headers || '{}'); + } catch (e) { + // Invalid headers JSON + } - // Stream the response directly - response.body.pipe(res); - } catch (error) { - console.log('[TS Error]:', error.message); - res.status(500).json( - new ErrorObject( - `TS Proxy unexpected error: ${error.message}`, - 'TS Proxy', - 500, - 'Check implementation or site status', - true, - true - ).toJSON() - ); + if (!targetUrl) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'URL parameter required' })); + return; } + + proxyTs(targetUrl, headers, req, res); }); - // HLS Proxy endpoint - app.get('/proxy/hls', cors(), async (req, res) => { + // HLS Proxy endpoint (alternative endpoint) + app.get('/proxy/hls', (req, res) => { + if (handleCors(req, res)) return; + const targetUrl = req.query.link; let headers = {}; try { headers = JSON.parse(req.query.headers || '{}'); } catch (e) { - console.log( - 'Invalid headers JSON for HLS proxy:', - req.query.headers - ); + // Invalid headers JSON } if (!targetUrl) { - return res - .status(400) - .json({ error: 'Link parameter is required' }); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Link parameter is required' })); + return; } - try { - console.log(`[HLS Proxy] Fetching: ${targetUrl}`); - console.log(`[HLS Proxy] Headers: ${JSON.stringify(headers)}`); - - const response = await fetch(targetUrl, { - headers: { - 'User-Agent': DEFAULT_USER_AGENT, - ...headers - } - }); - - console.log( - `[HLS Proxy] Response: ${response.status} ${response.statusText}` - ); - console.log('[HLS Proxy] Response Headers: '); - response.headers.forEach((v, k) => console.log(` ${k}: ${v}`)); - console.log('[HLS Proxy] Request Headers: ', headers); - - if (!response.ok) { - console.log( - `[HLS Proxy] Error: ${response.status} for ${targetUrl}` - ); - return res.status(response.status).json({ - error: `Failed to fetch HLS: ${response.status}` - }); - } - - let m3u8Content = await response.text(); - - const lines = m3u8Content.split('\n'); - const newLines = []; - - for (const line of lines) { - if (line.startsWith('#')) { - // Handle encryption keys - if (line.startsWith('#EXT-X-KEY:')) { - const regex = /https?:\/\/[^""\s]+/g; - const keyUrl = regex.exec(line)?.[0]; - if (keyUrl) { - const proxyUrl = `/ts-proxy?url=${encodeURIComponent( - keyUrl - )}&headers=${encodeURIComponent(JSON.stringify(headers))}`; - newLines.push(line.replace(keyUrl, proxyUrl)); - } else { - newLines.push(line); - } - } else { - newLines.push(line); - } - } else if (line.trim()) { - // Handle segment URLs - try { - const segmentUrl = new URL(line, targetUrl).href; - const proxyUrl = `/ts-proxy?url=${encodeURIComponent( - segmentUrl - )}&headers=${encodeURIComponent(JSON.stringify(headers))}`; - newLines.push(proxyUrl); - } catch { - newLines.push(line); // Keep original if URL parsing fails - } - } else { - newLines.push(line); // Keep empty lines - } - } + const protocol = req.headers['x-forwarded-proto'] || req.protocol || 'http'; + const host = req.headers.host; + const serverUrl = `${protocol}://${host}`; - res.setHeader('Content-Type', 'application/vnd.apple.mpegurl'); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Headers', '*'); - res.setHeader('Access-Control-Allow-Methods', '*'); - // Set filename - res.setHeader( - 'Content-Disposition', - `attachment; filename="master.m3u8"` - ); - - console.log( - `[HLS Proxy] Successfully proxied HLS for: ${targetUrl}` - ); - res.send(newLines.join('\n')); - } catch (error) { - console.error('[HLS Proxy Error]:', error.message); - res.status(500).json( - new ErrorObject( - `HLS Proxy unexpected error: ${error.message}`, - 'HLS Proxy', - 500, - 'Check implementation or site status', - true, - true - ).toJSON() - ); - } + proxyM3U8(targetUrl, headers, res, serverUrl); }); - // subtitle Proxy endpoint - app.get('/sub-proxy', cors(), async (req, res) => { + + // Subtitle Proxy endpoint + app.get('/sub-proxy', (req, res) => { + if (handleCors(req, res)) return; + const targetUrl = req.query.url; let headers = {}; try { headers = JSON.parse(req.query.headers || '{}'); } catch (e) { - console.log( - 'invalid headers JSON for subtitle proxy:', - req.query.headers - ); + // Invalid headers JSON } if (!targetUrl) { - return res.status(400).json({ error: 'url parameter required' }); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'url parameter required' })); + return; } - try { - console.log(`subtitle proxy fetching: ${targetUrl}`); - - const response = await fetch(targetUrl, { - headers: { - 'User-Agent': DEFAULT_USER_AGENT, - ...headers - } - }); - - console.log( - `subtitle proxy response: ${response.status} ${response.statusText}` - ); + fetch(targetUrl, { + headers: { + 'User-Agent': DEFAULT_USER_AGENT, + ...headers + } + }) + .then(response => { if (!response.ok) { - return res.status(response.status).json({ - error: `subtitle fetch failed: ${response.status}` - }); + res.writeHead(response.status); + res.end(`Subtitle fetch failed: ${response.status}`); + return; } - // Copy the content type from the upstream response - res.setHeader( - 'Content-Type', - response.headers.get('content-type') || 'text/vtt' - ); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Headers', '*'); - res.setHeader('Access-Control-Allow-Methods', '*'); + res.setHeader('Content-Type', response.headers.get('content-type') || 'text/vtt'); res.setHeader('Cache-Control', 'public, max-age=3600'); - res.setHeader( - 'Content-Disposition', - 'inline; filename="subtitle.vtt"' - ); - - // stream directly + + res.writeHead(200); response.body.pipe(res); - } catch (error) { - console.error('[Subtitle Proxy Error]:', error.message); - res.status(500).json( - new ErrorObject( - `Subtitle Proxy unexpected error: ${error.message}`, - 'Subtitle Proxy', - 500, - 'Check implementation or site status', - true, - true - ).toJSON() - ); - } + }) + .catch(error => { + console.error('[Sub Proxy Error]:', error.message); + res.writeHead(500); + res.end(`Subtitle Proxy error: ${error.message}`); + }); }); } @@ -606,8 +409,10 @@ export function processApiResponse(apiResponse, serverUrl) { finalUrl = extractOriginalUrl(finalUrl); // proxy ALL URLs through our system - if (!finalUrl.includes('.mp4') && !finalUrl.includes('.mkv')) { - // Use M3U8 proxy for HLS streams + if (finalUrl.includes('.m3u8') || finalUrl.includes('m3u8') || + (!finalUrl.includes('.mp4') && !finalUrl.includes('.mkv') && + !finalUrl.includes('.webm') && !finalUrl.includes('.avi'))) { + // Use M3U8 proxy for HLS streams and unknown formats const m3u8Origin = getOriginFromUrl(finalUrl); if (m3u8Origin) { proxyHeaders = { @@ -626,13 +431,15 @@ export function processApiResponse(apiResponse, serverUrl) { headers: proxyHeaders }; } else { - // we can Use TS proxy for direct video files + // Use TS proxy for direct video files (.mp4, .mkv, .webm, .avi) const videoOrigin = getOriginFromUrl(finalUrl); - proxyHeaders = { - ...proxyHeaders, - Referer: proxyHeaders.Referer || videoOrigin, - Origin: proxyHeaders.Origin || videoOrigin - }; + if (videoOrigin) { + proxyHeaders = { + ...proxyHeaders, + Referer: proxyHeaders.Referer || videoOrigin, + Origin: proxyHeaders.Origin || videoOrigin + }; + } const localProxyUrl = `${serverUrl}/ts-proxy?url=${encodeURIComponent(finalUrl)}&headers=${encodeURIComponent(JSON.stringify(proxyHeaders))}`; @@ -662,4 +469,4 @@ export function processApiResponse(apiResponse, serverUrl) { }; } -export { extractOriginalUrl }; +export { extractOriginalUrl }; \ No newline at end of file