Spaces:
Running
Running
addEventListener('fetch', event => { | |
event.respondWith(handleRequest(event.request)); | |
}); | |
async function handleRequest(request) { | |
const url = new URL(request.url); | |
const pathname = url.pathname; | |
// Handle the home page route | |
if (pathname === '/' && !url.search) { | |
const html = ` | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>M3U Playlist Proxy</title> | |
<style> | |
body{ | |
color: #626262; | |
} | |
/* General Form Styling */ | |
form { | |
max-width: 600px; | |
margin: 0 auto; | |
padding: 20px; | |
background-color: #f7f7f7; | |
border: 1px solid #ddd; | |
border-radius: 8px; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
} | |
/* Styling for Input Fields */ | |
input[type="text"] { | |
width: calc(50% - 10px); | |
padding: 10px; | |
font-size: 16px; | |
border: 1px solid #ccc; | |
border-radius: 4px; | |
margin-bottom: 10px; | |
color: #626262; | |
} | |
input[type="text"]:focus { | |
border-color: #007bff; | |
outline: none; | |
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); | |
} | |
/* Styling for Add More and Submit Buttons */ | |
button { | |
padding: 10px 15px; | |
font-size: 16px; | |
border: none; | |
border-radius: 4px; | |
cursor: pointer; | |
margin-top: 10px; | |
width: 45%; | |
margin-left: 20px; | |
align-content: center; | |
} | |
button#add-more { | |
background-color: #007bff; | |
color: white; | |
} | |
button[type="submit"] { | |
background-color: #28a745; | |
color: white; | |
} | |
button:hover { | |
opacity: 0.9; | |
} | |
button:focus { | |
outline: none; | |
} | |
/* Styling for Text Area */ | |
textarea { | |
width: 100%; | |
padding: 10px; | |
font-size: 16px; | |
border: 1px solid #ccc; | |
border-radius: 4px; | |
margin-top: 10px; | |
resize: none; | |
color: #626262; | |
} | |
/* Styling for Header Pair Div */ | |
.header-pair { | |
display: flex; | |
justify-content: space-between; | |
margin-bottom: 10px; | |
} | |
h3,h4 { | |
text-align: center; | |
font-size: 24px; | |
font-weight: bold; | |
margin-bottom: 20px; | |
color: #626262; | |
} | |
form { | |
max-width: 600px; | |
margin: 0 auto; | |
padding: 20px; | |
background-color: #f7f7f7; | |
border: 1px solid #ddd; | |
border-radius: 8px; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
} | |
.container { | |
max-width: 600px; | |
margin: 0 auto; | |
} | |
.footer { | |
max-width: 600px; | |
margin: 0 auto; | |
padding-top:20px; | |
padding-bottom:10px; | |
text-align:center; | |
} | |
textarea#result { | |
width: 100%; | |
max-width: 600px; | |
display: block; | |
margin: 20px auto; | |
padding: 10px; | |
font-size: 16px; | |
border | |
/* Label Styling */ | |
label { | |
font-weight: bold; | |
margin-bottom: 5px; | |
display: block; | |
} | |
/* Basic Responsive Design */ | |
@media (max-width: 600px) { | |
input[type="text"] { | |
width: 100%; | |
margin-bottom: 10px; | |
} | |
.header-pair { | |
flex-direction: column; | |
} | |
} | |
</style> | |
</head> | |
<body><h3>M3U Playlist Proxy</h3> | |
<div class="container"><p>Use the form below to generate a playlist with the necessary headers. For example, if the server requires a "Referer: http://example.com" header to allow streaming, you would enter "Referer" as the Header Name and "http://example.com" as the Header Value.</p></div> | |
<form id="headerForm"> | |
<div> | |
<label for="playlistUrl">Playlist URL:</label><br> | |
<input type="text" id="playlistUrl" name="playlistUrl" placeholder="Enter playlist URL" style="width: 96%; margin-bottom: 20px;"> | |
</div> | |
<div class="header-pair"> | |
<input type="text" name="headerName" placeholder="Header Name"> | |
<input type="text" name="headerValue" placeholder="Header Value"> | |
</div> | |
<button type="button" id="add-more">Add More Headers</button> | |
<button type="submit">Generate Playlist URL</button> | |
</form> | |
<h4>Generated Playlist URL:</h4> | |
<textarea id="result" rows="4" cols="80" readonly></textarea> | |
<div class="container"><p>Once the URL has been generated, you can use a URL shortener like <a href="https://tinyurl.com" target=_blank">TinyURL.com</a> to shorten it. This will make it much easier to add the URL as a M3U playlist within your IPTV application.</p></div> | |
<div class="footer">Created by <a href="https://github.com/dtankdempse/m3u-playlist-proxy">Tank Dempse</a></div> | |
<script> | |
// Function to add more header input fields | |
document.getElementById('add-more').addEventListener('click', function () { | |
const headerPair = document.createElement('div'); | |
headerPair.classList.add('header-pair'); | |
headerPair.innerHTML = | |
"<input type='text' name='headerName' placeholder='Header Name'>" + | |
"<input type='text' name='headerValue' placeholder='Header Value'>"; | |
document.getElementById('headerForm').insertBefore(headerPair, document.getElementById('add-more')); | |
}); | |
// Function to handle form submission and generate the full URL | |
document.getElementById('headerForm').addEventListener('submit', function (event) { | |
event.preventDefault(); | |
const playlistUrl = document.getElementById('playlistUrl').value.trim(); | |
if (!playlistUrl) { | |
alert('Please enter a Playlist URL.'); | |
return; | |
} | |
let headers = []; | |
const headerPairs = document.querySelectorAll('.header-pair'); | |
headerPairs.forEach(pair => { | |
const headerName = pair.querySelector('input[name="headerName"]').value; | |
const headerValue = pair.querySelector('input[name="headerValue"]').value; | |
if (headerName && headerValue) { | |
headers.push(headerName + "=" + headerValue); | |
} | |
}); | |
if (headers.length > 0) { | |
const headerString = headers.join('|'); | |
const base64Encoded = btoa(headerString); | |
const urlEncodedData = encodeURIComponent(base64Encoded); | |
const baseUrl = window.location.origin; | |
const fullUrl = baseUrl + "/playlist?url=" + encodeURIComponent(playlistUrl) + "&data=" + urlEncodedData; | |
document.getElementById('result').value = fullUrl; | |
} else { | |
alert('Please add at least one header.'); | |
} | |
}); | |
</script> | |
</body> | |
</html> | |
`; | |
return new Response(html, { headers: { 'Content-Type': 'text/html' } }); | |
} | |
// Handle the /playlist route | |
if (pathname === '/playlist') { | |
const urlParam = url.searchParams.get('url'); | |
const dataParam = url.searchParams.get('data'); | |
if (!urlParam) { | |
return new Response('URL parameter missing', { status: 400 }); | |
} | |
return handlePlaylistRequest(request, urlParam, dataParam); | |
} | |
// Handle proxied URL requests (same logic as before) | |
const params = url.searchParams; | |
const requestUrl = params.has('url') ? decodeURIComponent(params.get('url')) : null; | |
const secondaryUrl = params.has('url2') ? decodeURIComponent(params.get('url2')) : null; | |
const data = params.get('data') ? atob(params.get('data')) : null; // Decode base64 | |
const isMaster = !params.has('url2'); // Check if it's a master playlist | |
const finalRequestUrl = isMaster ? requestUrl : secondaryUrl; | |
if (finalRequestUrl) { | |
// Handle encryption key requests | |
if (params.has('key') && params.get('key') === 'true') { | |
return fetchEncryptionKey(finalRequestUrl, data); | |
} | |
// Set dataType based on whether it's a master playlist (text) or a segment (binary) | |
const dataType = isMaster ? 'text' : 'binary'; | |
// Fetch content from the URL with appropriate headers and dataType | |
const result = await fetchContent(finalRequestUrl, data, dataType); | |
if (result.status >= 400) { | |
return new Response(`Error: ${result.status}`, { status: result.status }); | |
} | |
let content = result.content; | |
// If it's a master playlist, rewrite the URLs and treat as text | |
if (isMaster) { | |
const baseUrl = new URL(result.finalUrl).origin; | |
const proxyUrl = `${url.origin}`; | |
content = rewriteUrls(content, baseUrl, proxyUrl, params.get('data')); | |
} | |
// Send the response with content and headers | |
return new Response(content, { headers: result.headers, status: result.status }); | |
} | |
return new Response('Bad Request', { status: 400 }); | |
} | |
// Function to fetch content from a URL using fetch | |
async function fetchContent(url, data, dataType = 'text') { | |
const headers = new Headers(); | |
if (data) { | |
const headersArray = data.split('|'); | |
headersArray.forEach(header => { | |
const [key, value] = header.split('='); | |
headers.append(key.trim(), value.trim().replace(/['"]/g, '')); | |
}); | |
} | |
const response = await fetch(url, { headers }); | |
const buffer = await response.arrayBuffer(); | |
let content; | |
if (dataType === 'binary') { | |
content = buffer; // Treat as binary | |
} else { | |
content = new TextDecoder('utf-8').decode(buffer); // Default to text | |
} | |
return { | |
content, | |
finalUrl: url, | |
status: response.status, | |
headers: response.headers, | |
}; | |
} | |
// Function to handle playlist requests | |
async function handlePlaylistRequest(request, playlistUrl, data) { | |
const result = await fetchContent(playlistUrl); | |
if (result.status !== 200) { | |
return new Response('Failed to fetch playlist', { status: 500 }); | |
} | |
let playlistContent = result.content; | |
const baseUrl = new URL(request.url).origin; | |
playlistContent = rewritePlaylistUrls(playlistContent, baseUrl, data); | |
return new Response(playlistContent, { headers: { 'Content-Type': 'text/plain' } }); | |
} | |
// Function to fetch encryption key | |
async function fetchEncryptionKey(url, data) { | |
const updatedUrl = url.includes('videohls.ru') ? url.replace('videohls.ru', 'onlinehdhls.ru') : url; | |
const result = await fetchContent(updatedUrl, data, 'binary'); // Pass 'binary' as the dataType | |
if (result.status >= 400) { | |
return new Response(`Failed to fetch encryption key: ${result.status}`, { status: 500 }); | |
} | |
return new Response(result.content, { headers: result.headers }); | |
} | |
// Rewrite URLs in the M3U8 playlist | |
function rewriteUrls(content, baseUrl, proxyUrl, data) { | |
const lines = content.split('\n'); | |
const rewrittenLines = []; | |
let isNextLineUri = false; | |
lines.forEach(line => { | |
if (line.startsWith('#')) { | |
if (line.includes('URI="')) { | |
const uriMatch = line.match(/URI="([^"]+)"/i); | |
let uri = uriMatch[1]; | |
if (!uri.startsWith('http')) { | |
uri = new URL(uri, baseUrl).href; | |
} | |
const rewrittenUri = `${proxyUrl}?url=${encodeURIComponent(uri)}&data=${encodeURIComponent(data)}${line.includes('#EXT-X-KEY') ? '&key=true' : ''}`; | |
line = line.replace(uriMatch[1], rewrittenUri); | |
} | |
rewrittenLines.push(line); | |
if (line.includes('#EXT-X-STREAM-INF')) { | |
isNextLineUri = true; | |
} | |
} else if (line.startsWith('http') || isNextLineUri) { | |
const urlParam = isNextLineUri ? 'url' : 'url2'; | |
let lineUrl = line; | |
if (!lineUrl.startsWith('http')) { | |
lineUrl = new URL(lineUrl, baseUrl).href; | |
} | |
const fullUrl = `${proxyUrl}?${urlParam}=${encodeURIComponent(lineUrl)}&data=${encodeURIComponent(data)}${urlParam === 'url' ? '&type=/index.m3u8' : '&type=/index.ts'}`; | |
rewrittenLines.push(fullUrl); | |
isNextLineUri = false; | |
} else { | |
rewrittenLines.push(line); | |
} | |
}); | |
return rewrittenLines.join('\n'); | |
} | |
// Rewrite playlist URLs and encode headers | |
function rewritePlaylistUrls(content, baseUrl, data) { | |
const lines = content.split('\n'); | |
const rewrittenLines = []; | |
lines.forEach(line => { | |
if (line.startsWith('#EXTINF')) { | |
rewrittenLines.push(line); | |
} else if (line.startsWith('http')) { | |
const headerSeparatorIndex = line.indexOf('|'); | |
const streamUrl = headerSeparatorIndex !== -1 ? line.substring(0, headerSeparatorIndex) : line; | |
let base64Data = ''; | |
if (data) { | |
base64Data = data; // Use provided data directly if it's passed | |
} else if (headerSeparatorIndex !== -1) { | |
const headersString = line.substring(headerSeparatorIndex + 1); | |
const headers = headersString | |
? headersString.split('|').map(header => { | |
const [key, value] = header.split('='); | |
const cleanValue = value ? value.replace(/^['"]|['"]$/g, '').trim() : ''; | |
return `${key.trim()}=${cleanValue}`; | |
}) | |
: []; | |
base64Data = headers.length > 0 ? btoa(headers.join('|')) : ''; | |
} | |
const newUrl = `${baseUrl}?url=${encodeURIComponent(streamUrl)}&data=${encodeURIComponent(base64Data)}`; | |
rewrittenLines.push(newUrl); | |
} else { | |
rewrittenLines.push(line); | |
} | |
}); | |
return rewrittenLines.join('\n'); | |
} |