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 = `
M3U Playlist Proxy
M3U Playlist Proxy
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.
Generated Playlist URL:
Once the URL has been generated, you can use a URL shortener like TinyURL.com to shorten it. This will make it much easier to add the URL as a M3U playlist within your IPTV application.
`;
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');
}