Files
UnHided/mediaflow_proxy/static/url_generator.html
UrloMythus cfc6bbabc9 update
2026-02-19 20:15:03 +01:00

3052 lines
192 KiB
HTML

<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MediaFlow URL Generator</title>
<link rel="icon" href="/logo.png" type="image/x-icon">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
'display': ['Satoshi', 'system-ui', 'sans-serif'],
'mono': ['JetBrains Mono', 'Fira Code', 'monospace'],
},
animation: {
'fade-in': 'fadeIn 0.3s ease-out',
'slide-up': 'slideUp 0.3s ease-out',
}
}
}
}
</script>
<style>
@import url('https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700&display=swap');
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.tab-active {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4);
}
.input-field {
transition: all 0.2s ease;
}
.input-field:focus {
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.3);
}
.url-output {
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 100%);
font-family: 'JetBrains Mono', monospace;
}
.copy-btn {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
transition: all 0.2s ease;
}
.copy-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
}
.generate-btn {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
transition: all 0.2s ease;
}
.generate-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(99, 102, 241, 0.5);
}
.checkbox-custom {
accent-color: #8b5cf6;
}
select option {
background-color: #1f2937;
color: white;
}
.collapsible-header {
cursor: pointer;
user-select: none;
}
.collapsible-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.collapsible-content.open {
max-height: 2000px;
}
</style>
</head>
<body class="bg-gradient-to-br from-slate-50 via-gray-100 to-slate-200 dark:from-slate-900 dark:via-gray-900 dark:to-slate-800 min-h-full font-display">
<!-- Theme Toggle -->
<div class="fixed top-4 right-4 z-50">
<button id="themeToggle" class="p-2.5 rounded-full bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 shadow-lg">
<svg id="sunIcon" class="w-5 h-5 text-amber-500 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>
<svg id="moonIcon" class="w-5 h-5 text-indigo-600 block dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
</svg>
</button>
</div>
<!-- Back to Home -->
<div class="fixed top-4 left-4 z-50">
<a href="/" class="flex items-center gap-2 px-4 py-2 rounded-full bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 shadow-lg text-gray-700 dark:text-gray-200 text-sm font-medium">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
Home
</a>
</div>
<main class="container mx-auto px-4 py-12 max-w-6xl">
<!-- Header -->
<div class="text-center mb-10 animate-fade-in">
<div class="flex items-center justify-center gap-3 mb-3">
<img src="/logo.png" alt="MediaFlow" class="w-12 h-12 rounded-xl shadow-lg">
<h1 class="text-4xl font-bold bg-gradient-to-r from-indigo-600 via-purple-600 to-pink-500 bg-clip-text text-transparent">
URL Generator
</h1>
</div>
<p class="text-gray-600 dark:text-gray-400 text-lg">
Generate proxy URLs, extractor links, and encoded URLs for MediaFlow
</p>
</div>
<!-- API Password (Global) -->
<div class="mb-6 animate-slide-up">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-5 border border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-3 mb-3">
<svg class="w-5 h-5 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
<label class="text-sm font-semibold text-gray-700 dark:text-gray-200">API Password</label>
<span class="text-xs text-gray-500 dark:text-gray-400">(optional - for protected instances)</span>
</div>
<input type="password" id="globalApiPassword" placeholder="Enter API password if required"
class="input-field w-full px-4 py-2.5 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
</div>
</div>
<!-- Tab Navigation -->
<div class="flex flex-wrap gap-2 mb-6 animate-slide-up" style="animation-delay: 0.1s;">
<button onclick="switchTab('proxy')" id="tab-proxy" class="tab-btn tab-active px-6 py-3 rounded-xl font-semibold text-sm transition-all duration-200">
🔗 Proxy URLs
</button>
<button onclick="switchTab('extractor')" id="tab-extractor" class="tab-btn px-6 py-3 rounded-xl font-semibold text-sm bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 shadow-md">
🎬 Extractor
</button>
<button onclick="switchTab('encoded')" id="tab-encoded" class="tab-btn px-6 py-3 rounded-xl font-semibold text-sm bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 shadow-md">
🔐 Encoded URL
</button>
<button onclick="switchTab('xc')" id="tab-xc" class="tab-btn px-6 py-3 rounded-xl font-semibold text-sm bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 shadow-md">
📺 XC Proxy
</button>
<button onclick="switchTab('acestream')" id="tab-acestream" class="tab-btn px-6 py-3 rounded-xl font-semibold text-sm bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 shadow-md">
🌐 Acestream
</button>
<button onclick="switchTab('telegram')" id="tab-telegram" class="tab-btn px-6 py-3 rounded-xl font-semibold text-sm bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 shadow-md">
<i class="fa-brands fa-telegram"></i> Telegram
</button>
</div>
<!-- Proxy URL Generator Tab -->
<div id="panel-proxy" class="tab-panel animate-fade-in">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-6 border border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold text-gray-800 dark:text-white mb-6 flex items-center gap-2">
<span class="w-8 h-8 rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-white text-sm">🔗</span>
Proxy URL Generator
</h2>
<!-- Proxy Type Selector -->
<div class="mb-6">
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Proxy Type</label>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<button onclick="selectProxyType('hls')" id="proxy-type-hls" class="proxy-type-btn active-proxy-type px-4 py-3 rounded-xl border-2 border-indigo-500 bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium text-sm transition-all">
📺 HLS Manifest
</button>
<button onclick="selectProxyType('mpd')" id="proxy-type-mpd" class="proxy-type-btn px-4 py-3 rounded-xl border-2 border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-700 dark:text-gray-300 font-medium text-sm transition-all hover:border-indigo-300">
🎥 MPD/DASH
</button>
<button onclick="selectProxyType('stream')" id="proxy-type-stream" class="proxy-type-btn px-4 py-3 rounded-xl border-2 border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-700 dark:text-gray-300 font-medium text-sm transition-all hover:border-indigo-300">
📡 Stream
</button>
</div>
</div>
<!-- HLS Form -->
<div id="form-hls" class="proxy-form space-y-4">
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Destination URL <span class="text-red-500">*</span></label>
<input type="url" id="hls-destination" placeholder="https://example.com/playlist.m3u8"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500">
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Key URL <span class="text-gray-400 font-normal">(optional)</span></label>
<input type="url" id="hls-key-url" placeholder="https://example.com/key"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Replace the original key URL (useful for bypassing protection)</p>
</div>
<!-- HLS Options -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Options</h4>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="hls-force-playlist" class="checkbox-custom w-4 h-4 rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">Force Playlist Proxy</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="hls-key-only" class="checkbox-custom w-4 h-4 rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">Key Only Proxy</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="hls-no-proxy" class="checkbox-custom w-4 h-4 rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">No Proxy</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="hls-max-res" class="checkbox-custom w-4 h-4 rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">Max Resolution</span>
</label>
</div>
<!-- Resolution Selection -->
<div class="mt-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Resolution <span class="text-gray-400 font-normal">(optional)</span></label>
<select id="hls-resolution" class="input-field w-full sm:w-48 px-4 py-2.5 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<option value="">Auto (no preference)</option>
<option value="2160p">2160p (4K)</option>
<option value="1440p">1440p (2K)</option>
<option value="1080p">1080p (Full HD)</option>
<option value="720p">720p (HD)</option>
<option value="480p">480p (SD)</option>
<option value="360p">360p</option>
<option value="240p">240p</option>
</select>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Select specific resolution (falls back to closest lower if not available)</p>
</div>
<div class="mt-3 text-xs text-gray-500 dark:text-gray-400 space-y-1">
<p><strong>Force Playlist Proxy:</strong> Force all playlist URLs to be proxied regardless of content routing settings</p>
<p><strong>Key Only Proxy:</strong> Only proxy the key URL, leaving segment URLs direct</p>
<p><strong>No Proxy:</strong> Returns manifest without proxying internal URLs</p>
<p><strong>Max Resolution:</strong> Redirects to the highest resolution stream</p>
</div>
</div>
<!-- Skip Segments Section -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Skip Segments <span class="text-gray-400 font-normal">(Intro/Outro Skip)</span></h4>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Time Ranges to Skip</label>
<input type="text" id="hls-skip-segments" placeholder="0-90,1750-1800 (start-end in seconds, comma-separated)"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Format: start-end,start-end (e.g., "0-90" skips first 90 seconds, "0-112,1750-1800" skips intro and outro). Supports decimal values.</p>
</div>
</div>
<!-- Start Offset Section (Live Streams) -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Start Offset <span class="text-gray-400 font-normal">(Live Stream Prebuffer)</span></h4>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Time Offset (seconds)</label>
<input type="number" id="hls-start-offset" placeholder="-18" step="0.1"
class="input-field w-full sm:w-48 px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Use negative values to start behind the live edge (e.g., -18 to start 18 seconds behind). This enables the prebuffer to work on live streams by creating headroom for prefetching segments.</p>
</div>
</div>
<!-- HLS Response Headers Section -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Response Headers <span class="text-gray-400 font-normal">(Advanced)</span></h4>
<div class="space-y-4">
<div>
<div class="flex items-center justify-between mb-2">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Add Response Headers <span class="text-gray-400 font-normal">(r_ prefix - manifest only)</span></label>
<button onclick="addHlsResponseHeader()" class="text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 font-medium flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
Add
</button>
</div>
<div id="hls-response-headers-container" class="space-y-2">
<!-- Response headers will be added here dynamically -->
</div>
</div>
<div>
<div class="flex items-center justify-between mb-2">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Propagate Headers to Segments <span class="text-gray-400 font-normal">(rp_ prefix)</span></label>
<button onclick="addHlsPropagateHeader()" class="text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 font-medium flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
Add
</button>
</div>
<div id="hls-propagate-headers-container" class="space-y-2">
<!-- Propagate headers will be added here dynamically -->
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Headers that propagate to segment URLs (e.g., content-type for disguised segments)</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Remove Response Headers <span class="text-gray-400 font-normal">(x_headers)</span></label>
<input type="text" id="hls-remove-headers" placeholder="content-length, content-range"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Comma-separated list of response headers to remove</p>
</div>
</div>
</div>
</div>
<!-- MPD Form -->
<div id="form-mpd" class="proxy-form space-y-4 hidden">
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Destination URL <span class="text-red-500">*</span></label>
<input type="url" id="mpd-destination" placeholder="https://example.com/manifest.mpd"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500">
</div>
<!-- DRM Section -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">DRM Parameters (Clear Key)</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Key ID</label>
<input type="text" id="mpd-key-id" placeholder="eb676abbcb345e96bbcf616630f1a3da"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 font-mono text-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Key</label>
<input type="text" id="mpd-key" placeholder="100b6c20940f779a4589152b57d2dacb"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 font-mono text-sm">
</div>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">For Clear Key DRM-protected streams. Leave empty for non-DRM content.</p>
</div>
<!-- Resolution Selection -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Resolution</h4>
<select id="mpd-resolution" class="input-field w-full sm:w-48 px-4 py-2.5 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<option value="">Auto (no preference)</option>
<option value="2160p">2160p (4K)</option>
<option value="1440p">1440p (2K)</option>
<option value="1080p">1080p (Full HD)</option>
<option value="720p">720p (HD)</option>
<option value="480p">480p (SD)</option>
<option value="360p">360p</option>
<option value="240p">240p</option>
</select>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">Select specific resolution (falls back to closest lower if not available)</p>
</div>
<!-- Skip Segments Section -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Skip Segments <span class="text-gray-400 font-normal">(Intro/Outro Skip)</span></h4>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Time Ranges to Skip</label>
<input type="text" id="mpd-skip-segments" placeholder="0-90,1750-1800 (start-end in seconds, comma-separated)"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Format: start-end,start-end (e.g., "0-90" skips first 90 seconds, "0-112,1750-1800" skips intro and outro). Supports decimal values.</p>
</div>
</div>
<!-- Start Offset Section (Live Streams) -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Start Offset <span class="text-gray-400 font-normal">(Live Stream Prebuffer)</span></h4>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Time Offset (seconds)</label>
<input type="number" id="mpd-start-offset" placeholder="-18" step="0.1"
class="input-field w-full sm:w-48 px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Use negative values to start behind the live edge (e.g., -18 to start 18 seconds behind). This enables the prebuffer to work on live streams by creating headroom for prefetching segments.</p>
</div>
</div>
<!-- MPD Response Headers Section -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Response Headers <span class="text-gray-400 font-normal">(Advanced)</span></h4>
<div class="space-y-4">
<div>
<div class="flex items-center justify-between mb-2">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Add Response Headers <span class="text-gray-400 font-normal">(r_ prefix)</span></label>
<button onclick="addMpdResponseHeader()" class="text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 font-medium flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
Add
</button>
</div>
<div id="mpd-response-headers-container" class="space-y-2">
<!-- Response headers will be added here dynamically -->
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Remove Response Headers <span class="text-gray-400 font-normal">(x_headers)</span></label>
<input type="text" id="mpd-remove-headers" placeholder="X-Frame-Options, Content-Security-Policy"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Comma-separated list of response headers to remove</p>
</div>
</div>
</div>
</div>
<!-- Stream Form -->
<div id="form-stream" class="proxy-form space-y-4 hidden">
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Destination URL <span class="text-red-500">*</span></label>
<input type="url" id="stream-destination" placeholder="https://example.com/video.mp4"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500">
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Filename <span class="text-gray-400 font-normal">(optional)</span></label>
<input type="text" id="stream-filename" placeholder="movie.mp4"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Filename for Content-Disposition header (useful for media players like Infuse)</p>
</div>
<!-- Transcode Section -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Transcode <span class="text-gray-400 font-normal">(optional)</span></h4>
<div class="space-y-4">
<div class="flex items-center gap-3">
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="stream-transcode" class="sr-only peer">
<div class="w-11 h-6 bg-gray-300 dark:bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-500"></div>
</label>
<span class="text-sm text-gray-700 dark:text-gray-300">Enable Transcoding</span>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400">
Transcode to browser-compatible fMP4 (H.264 + AAC). Without Start Time, generates
<code>/proxy/transcode/playlist.m3u8</code> (HLS). With Start Time, generates
<code>/proxy/stream?...&transcode=true&start=...</code>.
</p>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Start Time (seconds) <span class="text-gray-400 font-normal">(optional)</span></label>
<input type="number" id="stream-start-time" placeholder="0" min="0" step="1"
class="input-field w-full sm:w-48 px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Seek to this position before starting transcoded playback (e.g., 300 for 5 minutes)</p>
</div>
</div>
</div>
<!-- Response Headers Section -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Response Headers</h4>
<div class="space-y-4">
<div>
<div class="flex items-center justify-between mb-2">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Add Response Headers <span class="text-gray-400 font-normal">(r_ prefix)</span></label>
<button onclick="addStreamResponseHeader()" class="text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 font-medium flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
Add
</button>
</div>
<div id="stream-response-headers-container" class="space-y-2">
<!-- Response headers will be added here dynamically -->
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Headers to add to the proxied response (e.g., Cache-Control, Content-Type)</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Remove Response Headers <span class="text-gray-400 font-normal">(x_headers)</span></label>
<input type="text" id="stream-remove-headers" placeholder="X-Frame-Options, Content-Security-Policy"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Comma-separated list of response headers to remove from the proxied response</p>
</div>
</div>
</div>
</div>
<!-- Request Headers Section -->
<div class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between mb-4">
<label class="text-sm font-semibold text-gray-700 dark:text-gray-200">Request Headers <span class="text-gray-400 font-normal">(optional)</span></label>
<button onclick="addProxyHeader()" class="text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 font-medium flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
Add Header
</button>
</div>
<div id="proxy-headers-container" class="space-y-2">
<!-- Headers will be added here dynamically -->
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">Headers will be encoded as h_HeaderName=value in the URL</p>
</div>
<!-- Generate Button -->
<div class="mt-6">
<button onclick="generateProxyUrl()" class="generate-btn w-full py-3.5 rounded-xl text-white font-semibold text-sm">
Generate Proxy URL
</button>
</div>
<!-- Output -->
<div id="proxy-output" class="mt-6 hidden">
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Generated URL</label>
<div class="url-output rounded-xl p-4 text-green-400 text-sm break-all overflow-x-auto">
<code id="proxy-generated-url"></code>
</div>
<div class="flex flex-wrap gap-2 mt-3">
<button onclick="copyUrl('proxy-generated-url', event)" class="copy-btn px-6 py-2.5 rounded-xl text-white font-semibold text-sm">
📋 Copy to Clipboard
</button>
<button id="proxy-hls-preview-btn" onclick="openHlsPreview('proxy')" class="hidden px-6 py-2.5 rounded-xl bg-gradient-to-r from-green-500 to-emerald-600 text-white font-semibold text-sm hover:from-green-600 hover:to-emerald-700 transition-all">
▶ Preview (HLS)
</button>
</div>
</div>
<!-- HLS Preview Player -->
<div id="proxy-hls-player" class="mt-4 hidden">
<div class="bg-black rounded-xl overflow-hidden shadow-xl">
<video id="proxy-video" controls class="w-full max-h-[80vh] object-contain bg-black"></video>
</div>
<button onclick="closeHlsPreview('proxy')" class="mt-2 px-4 py-2 rounded-lg bg-gray-600 text-white text-sm hover:bg-gray-700 transition-all">
Close Preview
</button>
</div>
</div>
</div>
<!-- Extractor Tab -->
<div id="panel-extractor" class="tab-panel hidden animate-fade-in">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-6 border border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold text-gray-800 dark:text-white mb-6 flex items-center gap-2">
<span class="w-8 h-8 rounded-lg bg-gradient-to-br from-pink-500 to-rose-600 flex items-center justify-center text-white text-sm">🎬</span>
Extractor URL Generator
</h2>
<div class="space-y-4">
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Video Host <span class="text-red-500">*</span></label>
<select id="extractor-host" onchange="onExtractorHostChange()" class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500">
<option value="">Select a host...</option>
<option value="Doodstream">Doodstream</option>
<option value="FileLions">FileLions</option>
<option value="FileMoon">FileMoon</option>
<option value="F16Px">F16Px</option>
<option value="Mixdrop">Mixdrop</option>
<option value="Uqload">Uqload</option>
<option value="Streamtape">Streamtape</option>
<option value="StreamWish">StreamWish</option>
<option value="Supervideo">Supervideo</option>
<option value="VixCloud">VixCloud</option>
<option value="Okru">Okru</option>
<option value="Maxstream">Maxstream</option>
<option value="LiveTV">LiveTV</option>
<option value="LuluStream">LuluStream</option>
<option value="DLHD">DLHD</option>
<option value="Fastream">Fastream</option>
<option value="TurboVidPlay">TurboVidPlay</option>
<option value="Vidmoly">Vidmoly</option>
<option value="Vidoza">Vidoza</option>
<option value="Voe">Voe</option>
<option value="Sportsonline">Sportsonline</option>
</select>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Video URL <span class="text-red-500">*</span></label>
<input type="url" id="extractor-destination" placeholder="https://doodstream.com/e/xxxxx"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500">
</div>
<!-- Extractor Options -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Options</h4>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="extractor-redirect" class="checkbox-custom w-4 h-4 rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">Redirect to Stream</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="extractor-max-res" class="checkbox-custom w-4 h-4 rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">Max Resolution</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="extractor-no-proxy" class="checkbox-custom w-4 h-4 rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">No Proxy</span>
</label>
</div>
<!-- URL Extension -->
<div class="mt-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">URL Extension <span class="text-gray-400 font-normal">(for player compatibility)</span></label>
<select id="extractor-extension" class="input-field w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<option value="">None (default)</option>
<option value="m3u8">HLS (.m3u8) - for ExoPlayer/Android</option>
<option value="mp4">MP4 (.mp4)</option>
<option value="ts">MPEG-TS (.ts)</option>
<option value="mkv">MKV (.mkv)</option>
<option value="webm">WebM (.webm)</option>
<option value="avi">AVI (.avi)</option>
</select>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
<strong>Tip:</strong> Use <code>.m3u8</code> for HLS streams when using ExoPlayer (Android/Stremio).
The extension helps players detect the stream type from the URL.
</p>
</div>
<div class="mt-3 text-xs text-gray-500 dark:text-gray-400 space-y-1">
<p><strong>Redirect to Stream:</strong> Automatically redirect to the stream endpoint after extraction</p>
<p><strong>Max Resolution:</strong> Select the highest resolution available</p>
<p><strong>No Proxy:</strong> Return direct URLs without proxying</p>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Extra Parameters <span class="text-gray-400 font-normal">(JSON, optional)</span></label>
<textarea id="extractor-extra-params" rows="3" placeholder='{"stream_title": "My Stream"}'
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 font-mono text-sm"></textarea>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Additional parameters for specific extractors (e.g., stream_title for LiveTV)</p>
</div>
<!-- Extractor Headers Section -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Headers <span class="text-gray-400 font-normal">(Advanced)</span></h4>
<div class="space-y-4">
<!-- Request Headers -->
<div>
<div class="flex items-center justify-between mb-2">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Request Headers <span class="text-gray-400 font-normal">(h_ prefix)</span></label>
<button onclick="addExtractorRequestHeader()" class="text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 font-medium flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
Add
</button>
</div>
<div id="extractor-request-headers-container" class="space-y-2">
<!-- Request headers will be added here dynamically -->
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Headers sent when extracting the stream (e.g., Referer, User-Agent)</p>
</div>
<!-- Response Headers -->
<div>
<div class="flex items-center justify-between mb-2">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Response Headers <span class="text-gray-400 font-normal">(r_ prefix)</span></label>
<button onclick="addExtractorResponseHeader()" class="text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 font-medium flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
Add
</button>
</div>
<div id="extractor-response-headers-container" class="space-y-2">
<!-- Response headers will be added here dynamically -->
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Headers to add to the proxied response (used with redirect_stream)</p>
</div>
<!-- Remove Headers -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Remove Response Headers <span class="text-gray-400 font-normal">(x_headers)</span></label>
<input type="text" id="extractor-remove-headers" placeholder="X-Frame-Options, Content-Security-Policy"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Comma-separated list of response headers to remove</p>
</div>
</div>
</div>
</div>
<!-- Generate Button -->
<div class="mt-6">
<button onclick="generateExtractorUrl()" class="generate-btn w-full py-3.5 rounded-xl text-white font-semibold text-sm">
Generate Extractor URL
</button>
</div>
<!-- Output -->
<div id="extractor-output" class="mt-6 hidden">
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Generated URL</label>
<div class="url-output rounded-xl p-4 text-green-400 text-sm break-all overflow-x-auto">
<code id="extractor-generated-url"></code>
</div>
<button onclick="copyUrl('extractor-generated-url', event)" class="copy-btn mt-3 px-6 py-2.5 rounded-xl text-white font-semibold text-sm">
📋 Copy to Clipboard
</button>
</div>
</div>
</div>
<!-- Encoded URL Tab -->
<div id="panel-encoded" class="tab-panel hidden animate-fade-in">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-6 border border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold text-gray-800 dark:text-white mb-6 flex items-center gap-2">
<span class="w-8 h-8 rounded-lg bg-gradient-to-br from-emerald-500 to-teal-600 flex items-center justify-center text-white text-sm">🔐</span>
Encoded URL Generator
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">Generate encrypted/encoded URLs with expiration, IP restriction, and custom headers using the server-side API.</p>
<div class="space-y-4">
<!-- Basic Settings -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">MediaFlow Proxy URL <span class="text-red-500">*</span></label>
<input type="url" id="encoded-proxy-url" placeholder="https://your-mediaflow-server.com"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500">
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Endpoint <span class="text-red-500">*</span></label>
<select id="encoded-endpoint" class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500">
<option value="">Select an endpoint...</option>
<option value="/proxy/hls/manifest.m3u8">HLS Manifest (/proxy/hls/manifest.m3u8)</option>
<option value="/proxy/mpd/manifest.m3u8">MPD Manifest (/proxy/mpd/manifest.m3u8)</option>
<option value="/proxy/stream">Stream (/proxy/stream)</option>
<option value="/extractor/video">Extractor (/extractor/video)</option>
<option value="/extractor/video.m3u8">Extractor HLS (/extractor/video.m3u8)</option>
<option value="/extractor/video.mp4">Extractor MP4 (/extractor/video.mp4)</option>
</select>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Destination URL <span class="text-red-500">*</span></label>
<input type="url" id="encoded-destination" placeholder="https://example.com/stream.m3u8"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500">
</div>
<!-- Query Parameters -->
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Query Parameters <span class="text-gray-400 font-normal">(JSON, optional)</span></label>
<textarea id="encoded-query-params" rows="2" placeholder='{"key_id": "xxx", "key": "yyy", "host": "Doodstream"}'
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 font-mono text-sm"></textarea>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Additional query parameters to include in the URL</p>
</div>
<!-- Headers Section -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Headers</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Request Headers <span class="text-gray-400 font-normal">(JSON)</span></label>
<textarea id="encoded-request-headers" rows="3" placeholder='{"Referer": "https://example.com", "User-Agent": "Mozilla/5.0"}'
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 font-mono text-sm"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Response Headers <span class="text-gray-400 font-normal">(JSON, r_ prefix)</span></label>
<textarea id="encoded-response-headers" rows="3" placeholder='{"Cache-Control": "no-cache"}'
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 font-mono text-sm"></textarea>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Headers for manifest response only</p>
</div>
</div>
<div class="mt-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Propagate Headers to Segments <span class="text-gray-400 font-normal">(JSON, rp_ prefix)</span></label>
<textarea id="encoded-propagate-headers" rows="2" placeholder='{"content-type": "video/mp2t"}'
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 font-mono text-sm"></textarea>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Headers that propagate to segment URLs (useful for disguised segments)</p>
</div>
<div class="mt-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Remove Response Headers <span class="text-gray-400 font-normal">(comma-separated)</span></label>
<input type="text" id="encoded-remove-headers" placeholder="content-length, content-range"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Response header names to remove from the proxied response</p>
</div>
</div>
<!-- Security & Advanced Options -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Security & Advanced Options</h4>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Expiration <span class="text-gray-400 font-normal">(seconds)</span></label>
<input type="number" id="encoded-expiration" placeholder="3600"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">URL expires after this time</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">IP Restriction</label>
<input type="text" id="encoded-ip" placeholder="192.168.1.1"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Restrict URL to this IP</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Filename</label>
<input type="text" id="encoded-filename" placeholder="video.mp4"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">For media players</p>
</div>
</div>
<div class="mt-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="encoded-base64" class="checkbox-custom w-4 h-4 rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">Base64 encode destination URL</span>
</label>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 ml-6">Encode the destination URL in base64 format before processing</p>
</div>
</div>
</div>
<!-- Generate Button -->
<div class="mt-6">
<button onclick="generateEncodedUrl()" class="generate-btn w-full py-3.5 rounded-xl text-white font-semibold text-sm">
Generate Encoded URL
</button>
</div>
<!-- Output -->
<div id="encoded-output" class="mt-6 hidden">
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Generated URL</label>
<div class="url-output rounded-xl p-4 text-green-400 text-sm break-all overflow-x-auto">
<code id="encoded-generated-url"></code>
</div>
<button onclick="copyUrl('encoded-generated-url', event)" class="copy-btn mt-3 px-6 py-2.5 rounded-xl text-white font-semibold text-sm">
📋 Copy to Clipboard
</button>
</div>
</div>
</div>
<!-- XC Proxy Tab -->
<div id="panel-xc" class="tab-panel hidden animate-fade-in">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-6 border border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold text-gray-800 dark:text-white mb-6 flex items-center gap-2">
<span class="w-8 h-8 rounded-lg bg-gradient-to-br from-orange-500 to-red-600 flex items-center justify-center text-white text-sm">📺</span>
Xtream Codes (XC) Proxy Configuration
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">Generate configuration for IPTV players to use MediaFlow as an XC API proxy. Supports live streams, VOD, series, and catch-up/timeshift.</p>
<div class="space-y-4">
<!-- XC Provider URL -->
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">XC Provider URL <span class="text-red-500">*</span></label>
<input type="url" id="xc-provider-url" placeholder="http://provider.com:8080"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Your IPTV provider's Xtream Codes server URL (include port if needed)</p>
</div>
<!-- XC Credentials -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">XC Username <span class="text-red-500">*</span></label>
<input type="text" id="xc-username" placeholder="your_xc_username"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500">
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">XC Password <span class="text-red-500">*</span></label>
<input type="password" id="xc-password" placeholder="your_xc_password"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500">
</div>
</div>
<!-- Info Box -->
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-xl p-4 border border-blue-200 dark:border-blue-800">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-blue-500 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div class="text-sm text-blue-800 dark:text-blue-200">
<p class="font-semibold mb-1">How it works</p>
<p>MediaFlow acts as a stateless proxy between your IPTV player and XC provider. Your credentials are encoded in the username field, allowing the player to connect through MediaFlow without any database or storage.</p>
</div>
</div>
</div>
</div>
<!-- Generate Button -->
<div class="mt-6">
<button onclick="generateXcConfig()" class="generate-btn w-full py-3.5 rounded-xl text-white font-semibold text-sm">
Generate Configuration
</button>
</div>
<!-- Output -->
<div id="xc-output" class="mt-6 hidden">
<!-- Configuration Card -->
<div class="bg-gradient-to-br from-orange-50 to-red-50 dark:from-orange-900/20 dark:to-red-900/20 rounded-xl p-6 border border-orange-200 dark:border-orange-800 mb-6">
<h3 class="text-lg font-bold text-gray-800 dark:text-white mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
IPTV Player Configuration
</h3>
<div class="space-y-4">
<!-- Server URL -->
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Server URL</label>
<div class="flex gap-2">
<div class="url-output flex-1 rounded-xl px-4 py-3 text-green-400 text-sm break-all">
<code id="xc-server-url"></code>
</div>
<button onclick="copyXcField('xc-server-url', event)" class="copy-btn px-4 py-2 rounded-xl text-white font-semibold text-sm flex-shrink-0">
📋
</button>
</div>
</div>
<!-- Username -->
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Username</label>
<div class="flex gap-2">
<div class="url-output flex-1 rounded-xl px-4 py-3 text-green-400 text-sm break-all">
<code id="xc-generated-username"></code>
</div>
<button onclick="copyXcField('xc-generated-username', event)" class="copy-btn px-4 py-2 rounded-xl text-white font-semibold text-sm flex-shrink-0">
📋
</button>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Base64-encoded string (compatible with all IPTV apps)</p>
</div>
<!-- Password -->
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Password</label>
<div class="flex gap-2">
<div class="url-output flex-1 rounded-xl px-4 py-3 text-green-400 text-sm break-all">
<code id="xc-generated-password"></code>
</div>
<button onclick="copyXcField('xc-generated-password', event)" class="copy-btn px-4 py-2 rounded-xl text-white font-semibold text-sm flex-shrink-0">
📋
</button>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Your original XC password (unchanged)</p>
</div>
</div>
</div>
<!-- Player-specific Instructions -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Player Setup Instructions
</h4>
<div class="space-y-3 text-sm">
<div class="collapsible">
<div class="collapsible-header flex items-center justify-between p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors" onclick="toggleCollapsible(this)">
<span class="font-medium text-gray-700 dark:text-gray-300">📱 TiviMate</span>
<svg class="w-4 h-4 text-gray-500 transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</div>
<div class="collapsible-content">
<div class="p-3 text-gray-600 dark:text-gray-400">
<ol class="list-decimal list-inside space-y-1">
<li>Open TiviMate → Add Playlist</li>
<li>Select "Xtream Codes Login"</li>
<li>Enter the Server URL, Username, and Password from above</li>
<li>Click "Next" to connect</li>
</ol>
</div>
</div>
</div>
<div class="collapsible">
<div class="collapsible-header flex items-center justify-between p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors" onclick="toggleCollapsible(this)">
<span class="font-medium text-gray-700 dark:text-gray-300">📺 IPTV Smarters</span>
<svg class="w-4 h-4 text-gray-500 transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</div>
<div class="collapsible-content">
<div class="p-3 text-gray-600 dark:text-gray-400">
<ol class="list-decimal list-inside space-y-1">
<li>Open IPTV Smarters → Add User</li>
<li>Select "Xtream Codes API"</li>
<li>Enter any name for the playlist</li>
<li>Enter the Username, Password, and URL from above</li>
<li>Click "Add User"</li>
</ol>
</div>
</div>
</div>
<div class="collapsible">
<div class="collapsible-header flex items-center justify-between p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors" onclick="toggleCollapsible(this)">
<span class="font-medium text-gray-700 dark:text-gray-300">🧭 OTT Navigator</span>
<svg class="w-4 h-4 text-gray-500 transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</div>
<div class="collapsible-content">
<div class="p-3 text-gray-600 dark:text-gray-400">
<ol class="list-decimal list-inside space-y-1">
<li>Open OTT Navigator → Settings → Providers</li>
<li>Add Provider → Select "Xtream"</li>
<li>Portal URL: Enter the Server URL from above</li>
<li>Login: Enter the Username from above</li>
<li>Password: Enter the Password from above</li>
<li>Save and refresh</li>
</ol>
</div>
</div>
</div>
<div class="collapsible">
<div class="collapsible-header flex items-center justify-between p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors" onclick="toggleCollapsible(this)">
<span class="font-medium text-gray-700 dark:text-gray-300">🎯 Other XC-Compatible Players</span>
<svg class="w-4 h-4 text-gray-500 transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</div>
<div class="collapsible-content">
<div class="p-3 text-gray-600 dark:text-gray-400">
<p class="mb-2">For any XC-compatible player, use these settings:</p>
<ul class="list-disc list-inside space-y-1">
<li><strong>Server/Portal URL:</strong> Use the Server URL above</li>
<li><strong>Username/Login:</strong> Use the generated Username above</li>
<li><strong>Password:</strong> Use your original XC password</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Supported Features -->
<div class="mt-4 bg-green-50 dark:bg-green-900/20 rounded-xl p-4 border border-green-200 dark:border-green-800">
<h4 class="text-sm font-semibold text-green-800 dark:text-green-200 mb-2">✅ Supported Features</h4>
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm text-green-700 dark:text-green-300">
<span>• Live TV</span>
<span>• VOD/Movies</span>
<span>• TV Series</span>
<span>• Catch-up/Timeshift</span>
<span>• EPG/TV Guide</span>
<span>• M3U Export</span>
<span>• Categories</span>
<span>• Account Info</span>
</div>
</div>
</div>
</div>
</div>
<!-- Acestream Tab -->
<div id="panel-acestream" class="tab-panel hidden animate-fade-in">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-6 border border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold text-gray-800 dark:text-white mb-6 flex items-center gap-2">
<span class="w-8 h-8 rounded-lg bg-gradient-to-br from-teal-500 to-cyan-600 flex items-center justify-center text-white text-sm">🌐</span>
Acestream Proxy URL Generator
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">Generate URLs to proxy Acestream P2P content through MediaFlow. Supports both HLS manifest and MPEG-TS stream output.</p>
<div class="space-y-4">
<!-- Stream Type Selection -->
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Output Format</label>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<button onclick="selectAcestreamType('ts')" id="acestream-type-ts" class="acestream-type-btn active-acestream-type px-4 py-3 rounded-xl border-2 border-teal-500 bg-teal-50 dark:bg-teal-900/30 text-teal-700 dark:text-teal-300 font-medium text-sm transition-all">
📡 MPEG-TS Stream (Recommended)
</button>
<button onclick="selectAcestreamType('hls')" id="acestream-type-hls" class="acestream-type-btn px-4 py-3 rounded-xl border-2 border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-700 dark:text-gray-300 font-medium text-sm transition-all hover:border-teal-300">
📺 HLS Manifest
</button>
</div>
</div>
<!-- Content ID Type Selection -->
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Content Identifier Type</label>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<button onclick="selectAcestreamIdType('content_id')" id="acestream-id-type-content_id" class="acestream-id-type-btn active-acestream-id-type px-4 py-3 rounded-xl border-2 border-cyan-500 bg-cyan-50 dark:bg-cyan-900/30 text-cyan-700 dark:text-cyan-300 font-medium text-sm transition-all">
🔑 Content ID
</button>
<button onclick="selectAcestreamIdType('infohash')" id="acestream-id-type-infohash" class="acestream-id-type-btn px-4 py-3 rounded-xl border-2 border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-700 dark:text-gray-300 font-medium text-sm transition-all hover:border-cyan-300">
🧲 Infohash (Magnet)
</button>
</div>
</div>
<!-- Content ID Input -->
<div id="acestream-content-id-input">
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Content ID <span class="text-red-500">*</span></label>
<input type="text" id="acestream-content-id" placeholder="Enter Acestream content ID"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-teal-500 font-mono text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">The content ID from acestream:// links</p>
</div>
<!-- Infohash Input (hidden by default) -->
<div id="acestream-infohash-input" class="hidden">
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Infohash <span class="text-red-500">*</span></label>
<input type="text" id="acestream-infohash" placeholder="Enter 40-character infohash"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-teal-500 font-mono text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">40-character hex string from magnet links (e.g., b04372b9543d763bd2dbd2a1842d9723fd080076)</p>
</div>
<!-- Transcode Section -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Transcode <span class="text-gray-400 font-normal">(optional)</span></h4>
<div class="space-y-4">
<div class="flex items-center gap-3">
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="acestream-transcode" class="sr-only peer">
<div class="w-11 h-6 bg-gray-300 dark:bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-teal-500"></div>
</label>
<span class="text-sm text-gray-700 dark:text-gray-300">Enable Transcoding</span>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400">
Transcode to browser-compatible fMP4 (H.264 + AAC) using direct stream mode
(<code>transcode=true</code>). Uses GPU acceleration when available.
</p>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Start Time (seconds) <span class="text-gray-400 font-normal">(optional)</span></label>
<input type="number" id="acestream-start-time" placeholder="0" min="0" step="1"
class="input-field w-full sm:w-48 px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-teal-500 text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Seek to this position before starting transcoded playback (e.g., 300 for 5 minutes)</p>
</div>
</div>
</div>
<!-- Start Offset Section (Live Streams) -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Start Offset <span class="text-gray-400 font-normal">(Live Stream Prebuffer)</span></h4>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Time Offset (seconds)</label>
<input type="number" id="acestream-start-offset" placeholder="-18" step="0.1"
class="input-field w-full sm:w-48 px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-teal-500 text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Use negative values to start behind the live edge (e.g., -18 to start 18 seconds behind). This enables the prebuffer to work on live streams.</p>
</div>
</div>
<!-- Info Box -->
<div class="bg-teal-50 dark:bg-teal-900/20 rounded-xl p-4 border border-teal-200 dark:border-teal-800">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-teal-500 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div class="text-sm text-teal-800 dark:text-teal-200">
<p class="font-semibold mb-1">Requirements</p>
<p>Acestream proxy requires a running Acestream engine accessible from the MediaFlow server. Make sure <code class="bg-teal-100 dark:bg-teal-800 px-1 rounded">ENABLE_ACESTREAM=true</code> is set in your MediaFlow configuration.</p>
</div>
</div>
</div>
</div>
<!-- Generate Button -->
<div class="mt-6">
<button onclick="generateAcestreamUrl()" class="w-full py-3.5 rounded-xl text-white font-semibold text-sm bg-gradient-to-r from-teal-500 to-cyan-500 hover:from-teal-600 hover:to-cyan-600 transition-all shadow-lg hover:shadow-xl">
Generate Acestream URL
</button>
</div>
<!-- Output -->
<div id="acestream-output" class="mt-6 hidden">
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Generated URL</label>
<div class="url-output rounded-xl p-4 text-green-400 text-sm break-all overflow-x-auto">
<code id="acestream-generated-url"></code>
</div>
<button onclick="copyUrl('acestream-generated-url', event)" class="copy-btn mt-3 px-6 py-2.5 rounded-xl text-white font-semibold text-sm">
📋 Copy to Clipboard
</button>
<!-- Usage Examples -->
<div class="mt-4 bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Usage Examples</h4>
<div class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<p><strong>VLC:</strong> Media → Open Network Stream → Paste the URL</p>
<p><strong>mpv:</strong> <code class="bg-gray-200 dark:bg-gray-700 px-1 rounded">mpv "YOUR_URL"</code></p>
<p><strong>Kodi:</strong> Add as a network source or use with Stremio addon</p>
<p><strong>Web Player:</strong> Use with any HLS-compatible web player (hls.js, video.js)</p>
</div>
</div>
</div>
<!-- Supported Features -->
<div class="mt-6 bg-green-50 dark:bg-green-900/20 rounded-xl p-4 border border-green-200 dark:border-green-800">
<h4 class="text-sm font-semibold text-green-800 dark:text-green-200 mb-2">✅ Features</h4>
<div class="grid grid-cols-2 md:grid-cols-3 gap-2 text-sm text-green-700 dark:text-green-300">
<span>• HLS Output</span>
<span>• MPEG-TS Output</span>
<span>• Live Streams</span>
<span>• Session Management</span>
<span>• Cross-process Sync</span>
<span>• Auto Keepalive</span>
</div>
</div>
</div>
</div>
<!-- Telegram Tab -->
<div id="panel-telegram" class="tab-panel hidden animate-fade-in">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-6 border border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold text-gray-800 dark:text-white mb-6 flex items-center gap-2">
<span class="w-8 h-8 rounded-lg bg-gradient-to-br from-sky-500 to-blue-600 flex items-center justify-center text-white text-sm"><i class="fa-brands fa-telegram"></i></span>
Telegram Proxy URL Generator
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">Stream Telegram media (videos, documents, photos) with high-speed parallel downloads and full seeking support.</p>
<div class="space-y-4">
<!-- Input Type Selection -->
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Input Type</label>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<button onclick="selectTelegramInputType('url')" id="telegram-input-url" class="telegram-input-btn active-telegram-input px-4 py-3 rounded-xl border-2 border-sky-500 bg-sky-50 dark:bg-sky-900/30 text-sky-700 dark:text-sky-300 font-medium text-sm transition-all">
🔗 t.me URL
</button>
<button onclick="selectTelegramInputType('ids')" id="telegram-input-ids" class="telegram-input-btn px-4 py-3 rounded-xl border-2 border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-700 dark:text-gray-300 font-medium text-sm transition-all hover:border-sky-300">
🔢 Chat ID + Message ID
</button>
<button onclick="selectTelegramInputType('file_id')" id="telegram-input-file_id" class="telegram-input-btn px-4 py-3 rounded-xl border-2 border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-700 dark:text-gray-300 font-medium text-sm transition-all hover:border-sky-300">
📄 File ID
</button>
</div>
</div>
<!-- Telegram URL Input -->
<div id="telegram-url-input">
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Telegram URL <span class="text-red-500">*</span></label>
<input type="text" id="telegram-url" placeholder="https://t.me/channel/123 or https://t.me/c/123456789/456"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-sky-500 text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Supported formats: t.me/channel/123 (public), t.me/c/123456789/456 (private), t.me/username/123</p>
</div>
<!-- Chat ID + Message ID Input (hidden by default) -->
<div id="telegram-ids-input" class="hidden space-y-4">
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Chat ID <span class="text-red-500">*</span></label>
<input type="text" id="telegram-chat-id" placeholder="-100123456789 or @channelname"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-sky-500 font-mono text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Channel/group ID (with -100 prefix for supergroups) or @username</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Message ID <span class="text-red-500">*</span></label>
<input type="number" id="telegram-message-id" placeholder="12345"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-sky-500 font-mono text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">The message ID containing the media</p>
</div>
</div>
<!-- File ID Input (hidden by default) -->
<div id="telegram-fileid-input" class="hidden space-y-4">
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">File ID <span class="text-red-500">*</span></label>
<input type="text" id="telegram-file-id" placeholder="BQACAgIAAxkBAAIBZ2..."
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-sky-500 font-mono text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Bot API file_id string (base64-encoded)</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">File Size (bytes) <span class="text-red-500">*</span></label>
<input type="number" id="telegram-file-size" placeholder="1048576"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-sky-500 font-mono text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">File size is required for range requests (seeking). Get this from the Bot API file info.</p>
</div>
</div>
<!-- Custom Filename -->
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Custom Filename <span class="text-gray-400 font-normal">(optional)</span></label>
<input type="text" id="telegram-filename" placeholder="movie.mp4"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-sky-500 text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Override the filename for media players that use filename for metadata</p>
</div>
<!-- Transcode Section -->
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Transcode <span class="text-gray-400 font-normal">(optional)</span></h4>
<div class="space-y-4">
<div class="flex items-center gap-3">
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="telegram-transcode" class="sr-only peer">
<div class="w-11 h-6 bg-gray-300 dark:bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-sky-500"></div>
</label>
<span class="text-sm text-gray-700 dark:text-gray-300">Enable Transcoding</span>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400">
Transcode to browser-compatible fMP4 (H.264 + AAC). Without Start Time, generates
<code>/proxy/telegram/transcode/playlist.m3u8</code> (HLS). With Start Time, generates
<code>/proxy/telegram/stream?...&transcode=true&start=...</code>.
</p>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Start Time (seconds) <span class="text-gray-400 font-normal">(optional)</span></label>
<input type="number" id="telegram-start-time" placeholder="0" min="0" step="1"
class="input-field w-full sm:w-48 px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:border-sky-500 text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Seek to this position before starting transcoded playback (e.g., 300 for 5 minutes)</p>
</div>
</div>
</div>
<!-- URL Format Examples -->
<div class="bg-sky-50 dark:bg-sky-900/20 rounded-xl p-4 border border-sky-200 dark:border-sky-800">
<h4 class="text-sm font-semibold text-sky-800 dark:text-sky-200 mb-2">📋 Input Format Examples</h4>
<div class="space-y-2 text-sm text-sky-700 dark:text-sky-300 font-mono">
<p><code>https://t.me/channelname/123</code> - Public channel URL</p>
<p><code>https://t.me/c/123456789/456</code> - Private channel URL</p>
<p><code>chat_id=-1001234567890, message_id=123</code> - Direct IDs</p>
<p><code>file_id=BQACAgI..., file_size=1048576</code> - Bot API file_id</p>
</div>
</div>
<!-- Requirements Notice -->
<div class="bg-amber-50 dark:bg-amber-900/20 rounded-xl p-4 border border-amber-200 dark:border-amber-800">
<div class="flex items-start gap-3">
<span class="text-amber-500 mt-0.5">⚠️</span>
<div class="text-sm text-amber-800 dark:text-amber-200">
<p class="font-semibold mb-1">Requirements</p>
<p>Telegram proxy requires configuration on the server side. Make sure <code class="bg-amber-100 dark:bg-amber-800 px-1 rounded">ENABLE_TELEGRAM=true</code> is set along with your API credentials and session string.</p>
</div>
</div>
</div>
</div>
<!-- Generate Button -->
<div class="mt-6">
<button onclick="generateTelegramUrl()" class="w-full py-3.5 rounded-xl text-white font-semibold text-sm bg-gradient-to-r from-sky-500 to-blue-500 hover:from-sky-600 hover:to-blue-600 transition-all shadow-lg hover:shadow-xl">
Generate Telegram URL
</button>
</div>
<!-- Output -->
<div id="telegram-output" class="mt-6 hidden">
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-200 mb-2">Generated URL</label>
<div class="url-output rounded-xl p-4 text-green-400 text-sm break-all overflow-x-auto">
<code id="telegram-generated-url"></code>
</div>
<div class="flex flex-wrap gap-2 mt-3">
<button onclick="copyUrl('telegram-generated-url', event)" class="copy-btn px-6 py-2.5 rounded-xl text-white font-semibold text-sm">
📋 Copy to Clipboard
</button>
<button id="telegram-hls-preview-btn" onclick="openHlsPreview('telegram')" class="hidden px-6 py-2.5 rounded-xl bg-gradient-to-r from-green-500 to-emerald-600 text-white font-semibold text-sm hover:from-green-600 hover:to-emerald-700 transition-all">
▶ Preview (HLS)
</button>
</div>
<!-- HLS Preview Player -->
<div id="telegram-hls-player" class="mt-4 hidden">
<div class="bg-black rounded-xl overflow-hidden shadow-xl">
<video id="telegram-video" controls class="w-full max-h-[80vh] object-contain bg-black"></video>
</div>
<button onclick="closeHlsPreview('telegram')" class="mt-2 px-4 py-2 rounded-lg bg-gray-600 text-white text-sm hover:bg-gray-700 transition-all">
Close Preview
</button>
</div>
<!-- Usage Examples -->
<div class="mt-4 bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">Usage Examples</h4>
<div class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<p><strong>VLC:</strong> Media → Open Network Stream → Paste the URL</p>
<p><strong>mpv:</strong> <code class="bg-gray-200 dark:bg-gray-700 px-1 rounded">mpv "YOUR_URL"</code></p>
<p><strong>Infuse/Kodi:</strong> Add as network stream for full metadata support</p>
<p><strong>Web Player:</strong> Direct URL works in browsers with video tag</p>
</div>
</div>
</div>
<!-- Additional Endpoints -->
<div class="mt-6 bg-gray-50 dark:bg-gray-700/30 rounded-xl p-4 border border-gray-200 dark:border-gray-600">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200 mb-3">📡 Additional Endpoints</h4>
<div class="space-y-3 text-sm">
<div class="flex items-center justify-between p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600">
<div>
<span class="font-medium text-gray-700 dark:text-gray-200">Media Info</span>
<p class="text-xs text-gray-500 dark:text-gray-400">Get file metadata (size, duration, dimensions)</p>
</div>
<code class="text-xs text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-900/30 px-2 py-1 rounded">/proxy/telegram/info</code>
</div>
<div class="flex items-center justify-between p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600">
<div>
<span class="font-medium text-gray-700 dark:text-gray-200">Status Check</span>
<p class="text-xs text-gray-500 dark:text-gray-400">Check Telegram session status</p>
</div>
<code class="text-xs text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-900/30 px-2 py-1 rounded">/proxy/telegram/status</code>
</div>
</div>
</div>
<!-- Supported Features -->
<div class="mt-6 bg-green-50 dark:bg-green-900/20 rounded-xl p-4 border border-green-200 dark:border-green-800">
<h4 class="text-sm font-semibold text-green-800 dark:text-green-200 mb-2">✅ Features</h4>
<div class="grid grid-cols-2 md:grid-cols-3 gap-2 text-sm text-green-700 dark:text-green-300">
<span>• High-speed Downloads</span>
<span>• Parallel Connections</span>
<span>• Range Requests</span>
<span>• Seeking Support</span>
<span>• Video/Audio/Photos</span>
<span>• Private Channels</span>
</div>
</div>
</div>
<!-- Session String Generator Section -->
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-6 border border-gray-200 dark:border-gray-700 mt-6">
<h2 class="text-xl font-bold text-gray-800 dark:text-white mb-6 flex items-center gap-2">
<span class="w-8 h-8 rounded-lg bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center text-white text-sm">🔑</span>
Session String Generator
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">Generate a session string to authenticate MediaFlow with your Telegram account. You only need to do this once.</p>
<!-- Step 1: API Credentials -->
<div id="session-step-1" class="session-step">
<div class="flex items-center gap-3 mb-4">
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm">1</div>
<h4 class="font-semibold text-gray-800 dark:text-white">Enter API Credentials</h4>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Get your API credentials from <a href="https://my.telegram.org/apps" target="_blank" class="text-violet-600 dark:text-violet-400 hover:underline">my.telegram.org/apps</a></p>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">API ID <span class="text-red-500">*</span></label>
<input type="text" id="session-api-id" placeholder="12345678"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-violet-500 text-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">API Hash <span class="text-red-500">*</span></label>
<input type="text" id="session-api-hash" placeholder="0123456789abcdef0123456789abcdef"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-violet-500 font-mono text-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Authentication Method</label>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<button onclick="selectSessionAuthType('phone')" id="session-auth-phone" class="session-auth-btn active-session-auth px-4 py-3 rounded-xl border-2 border-violet-500 bg-violet-50 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300 font-medium text-sm transition-all">
📱 Phone Number (Full Access)
</button>
<button onclick="selectSessionAuthType('bot')" id="session-auth-bot" class="session-auth-btn px-4 py-3 rounded-xl border-2 border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-700 dark:text-gray-300 font-medium text-sm transition-all hover:border-violet-300">
🤖 Bot Token (Limited)
</button>
</div>
</div>
<!-- Phone Number Input -->
<div id="session-phone-input">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Phone Number <span class="text-red-500">*</span></label>
<input type="tel" id="session-phone" placeholder="+1234567890"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-violet-500 text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">International format with country code</p>
</div>
<!-- Bot Token Input (hidden by default) -->
<div id="session-bot-input" class="hidden">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Bot Token <span class="text-red-500">*</span></label>
<input type="text" id="session-bot-token" placeholder="123456789:ABCdefGHIjklMNOpqrsTUVwxyz"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-violet-500 font-mono text-sm">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Get from @BotFather on Telegram</p>
</div>
<button onclick="startSessionGeneration()" id="session-start-btn" class="w-full py-3 rounded-xl text-white font-semibold text-sm bg-gradient-to-r from-violet-500 to-purple-500 hover:from-violet-600 hover:to-purple-600 transition-all shadow-lg hover:shadow-xl">
<span id="session-start-btn-text">Send Verification Code</span>
<span id="session-start-btn-loading" class="hidden"><i class="fa-solid fa-spinner fa-spin mr-2"></i>Processing...</span>
</button>
</div>
</div>
<!-- Step 2: Verification Code -->
<div id="session-step-2" class="session-step hidden">
<div class="flex items-center gap-3 mb-4">
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm">2</div>
<h4 class="font-semibold text-gray-800 dark:text-white">Enter Verification Code</h4>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">A verification code has been sent to your Telegram app. Enter it below.</p>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Verification Code <span class="text-red-500">*</span></label>
<input type="text" id="session-code" placeholder="12345"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-violet-500 text-center text-2xl tracking-widest font-mono">
</div>
<div class="flex gap-3">
<button onclick="cancelSession()" class="flex-1 py-3 rounded-xl font-semibold text-sm border-2 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
Cancel
</button>
<button onclick="verifySessionCode()" id="session-verify-btn" class="flex-1 py-3 rounded-xl text-white font-semibold text-sm bg-gradient-to-r from-violet-500 to-purple-500 hover:from-violet-600 hover:to-purple-600 transition-all shadow-lg">
<span id="session-verify-btn-text">Verify Code</span>
<span id="session-verify-btn-loading" class="hidden"><i class="fa-solid fa-spinner fa-spin mr-2"></i>Verifying...</span>
</button>
</div>
</div>
</div>
<!-- Step 2b: 2FA Password -->
<div id="session-step-2fa" class="session-step hidden">
<div class="flex items-center gap-3 mb-4">
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm">2</div>
<h4 class="font-semibold text-gray-800 dark:text-white">Two-Factor Authentication</h4>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Your account has 2FA enabled. Please enter your cloud password.</p>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">2FA Password <span class="text-red-500">*</span></label>
<input type="password" id="session-2fa-password" placeholder="Enter your 2FA password"
class="input-field w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-violet-500 text-sm">
</div>
<div class="flex gap-3">
<button onclick="cancelSession()" class="flex-1 py-3 rounded-xl font-semibold text-sm border-2 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
Cancel
</button>
<button onclick="verify2FA()" id="session-2fa-btn" class="flex-1 py-3 rounded-xl text-white font-semibold text-sm bg-gradient-to-r from-violet-500 to-purple-500 hover:from-violet-600 hover:to-purple-600 transition-all shadow-lg">
<span id="session-2fa-btn-text">Verify Password</span>
<span id="session-2fa-btn-loading" class="hidden"><i class="fa-solid fa-spinner fa-spin mr-2"></i>Verifying...</span>
</button>
</div>
</div>
</div>
<!-- Step 3: Success - Show Session String -->
<div id="session-step-3" class="session-step hidden">
<div class="flex items-center gap-3 mb-4">
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center text-white font-bold text-sm"></div>
<h4 class="font-semibold text-gray-800 dark:text-white">Session String Generated!</h4>
</div>
<div class="bg-green-50 dark:bg-green-900/20 rounded-xl p-4 border border-green-200 dark:border-green-800 mb-4">
<p class="text-sm text-green-700 dark:text-green-300">Your session string has been generated successfully. Copy the configuration below and add it to your environment variables.</p>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Environment Configuration</label>
<div class="bg-gray-900 rounded-xl p-4 font-mono text-sm text-green-400 overflow-x-auto">
<div class="space-y-1">
<div>ENABLE_TELEGRAM=true</div>
<div>TELEGRAM_API_ID=<span id="session-result-api-id"></span></div>
<div>TELEGRAM_API_HASH=<span id="session-result-api-hash"></span></div>
<div class="break-all">TELEGRAM_SESSION_STRING=<span id="session-result-string"></span></div>
</div>
</div>
<button onclick="copySessionConfig(event)" class="copy-btn mt-3 px-6 py-2.5 rounded-xl text-white font-semibold text-sm">
📋 Copy Configuration
</button>
</div>
<button onclick="resetSessionGenerator()" class="w-full py-3 rounded-xl font-semibold text-sm border-2 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
Generate Another Session
</button>
</div>
</div>
<!-- Error Display -->
<div id="session-error" class="hidden mt-4 bg-red-50 dark:bg-red-900/20 rounded-xl p-4 border border-red-200 dark:border-red-800">
<div class="flex items-start gap-3">
<span class="text-red-500 mt-0.5"></span>
<div class="text-sm text-red-800 dark:text-red-200">
<p class="font-semibold">Error</p>
<p id="session-error-message"></p>
</div>
</div>
</div>
<!-- Security Notice -->
<div class="mt-6 bg-red-50 dark:bg-red-900/20 rounded-xl p-4 border border-red-200 dark:border-red-800">
<div class="flex items-start gap-3">
<span class="text-red-500 mt-0.5">🔒</span>
<div class="text-sm text-red-800 dark:text-red-200">
<p class="font-semibold mb-1">Security Notice</p>
<p>The session string is equivalent to your Telegram password. Keep it secret and never share it publicly. If compromised, revoke all sessions in your Telegram app settings.</p>
</div>
</div>
</div>
<!-- Check Status Section -->
<div class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<h4 class="font-semibold text-gray-800 dark:text-white mb-3">Check Connection Status</h4>
<button onclick="checkTelegramStatus()" class="px-4 py-2 rounded-lg bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300 text-sm font-medium hover:bg-violet-200 dark:hover:bg-violet-900/50 transition-colors">
🔍 Check Status
</button>
<div id="telegram-status-result" class="mt-3 hidden"></div>
</div>
</div>
</div>
</main>
<script>
// Theme management
const themeToggle = document.getElementById('themeToggle');
const html = document.documentElement;
const savedTheme = localStorage.getItem('theme') || 'light';
html.classList.toggle('dark', savedTheme === 'dark');
themeToggle.addEventListener('click', () => {
html.classList.toggle('dark');
const newTheme = html.classList.contains('dark') ? 'dark' : 'light';
localStorage.setItem('theme', newTheme);
});
// Tab switching
function switchTab(tabName) {
document.querySelectorAll('.tab-panel').forEach(panel => {
panel.classList.add('hidden');
});
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('tab-active');
btn.classList.add('bg-white', 'dark:bg-gray-800', 'text-gray-700', 'dark:text-gray-300', 'shadow-md');
});
document.getElementById(`panel-${tabName}`).classList.remove('hidden');
const activeTab = document.getElementById(`tab-${tabName}`);
activeTab.classList.add('tab-active');
activeTab.classList.remove('bg-white', 'dark:bg-gray-800', 'text-gray-700', 'dark:text-gray-300', 'shadow-md');
}
// Proxy type switching
function selectProxyType(type) {
document.querySelectorAll('.proxy-form').forEach(form => {
form.classList.add('hidden');
});
document.querySelectorAll('.proxy-type-btn').forEach(btn => {
btn.classList.remove('active-proxy-type', 'border-indigo-500', 'bg-indigo-50', 'dark:bg-indigo-900/30', 'text-indigo-700', 'dark:text-indigo-300');
btn.classList.add('border-gray-200', 'dark:border-gray-600', 'bg-gray-50', 'dark:bg-gray-700/50', 'text-gray-700', 'dark:text-gray-300');
});
document.getElementById(`form-${type}`).classList.remove('hidden');
const activeBtn = document.getElementById(`proxy-type-${type}`);
activeBtn.classList.add('active-proxy-type', 'border-indigo-500', 'bg-indigo-50', 'dark:bg-indigo-900/30', 'text-indigo-700', 'dark:text-indigo-300');
activeBtn.classList.remove('border-gray-200', 'dark:border-gray-600', 'bg-gray-50', 'dark:bg-gray-700/50', 'text-gray-700', 'dark:text-gray-300');
}
// Header management for proxy URLs
let proxyHeaderCount = 0;
let streamResponseHeaderCount = 0;
function addProxyHeader() {
const container = document.getElementById('proxy-headers-container');
const headerId = `proxy-header-${proxyHeaderCount++}`;
const headerRow = document.createElement('div');
headerRow.id = headerId;
headerRow.className = 'flex gap-2 items-center';
headerRow.innerHTML = `
<input type="text" placeholder="Header Name (e.g., Referer)" class="header-name flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<input type="text" placeholder="Header Value" class="header-value flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<button onclick="removeProxyHeader('${headerId}')" class="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
`;
container.appendChild(headerRow);
}
function removeProxyHeader(headerId) {
document.getElementById(headerId).remove();
}
function getProxyHeaders() {
const headers = {};
document.querySelectorAll('#proxy-headers-container > div').forEach(row => {
const name = row.querySelector('.header-name').value.trim();
const value = row.querySelector('.header-value').value.trim();
if (name && value) {
headers[name] = value;
}
});
return headers;
}
// Stream response headers management
function addStreamResponseHeader() {
const container = document.getElementById('stream-response-headers-container');
const headerId = `stream-response-header-${streamResponseHeaderCount++}`;
const headerRow = document.createElement('div');
headerRow.id = headerId;
headerRow.className = 'flex gap-2 items-center';
headerRow.innerHTML = `
<input type="text" placeholder="Header Name (e.g., Cache-Control)" class="response-header-name flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<input type="text" placeholder="Header Value" class="response-header-value flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<button onclick="removeStreamResponseHeader('${headerId}')" class="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
`;
container.appendChild(headerRow);
}
function removeStreamResponseHeader(headerId) {
document.getElementById(headerId).remove();
}
function getStreamResponseHeaders() {
const headers = {};
document.querySelectorAll('#stream-response-headers-container > div').forEach(row => {
const name = row.querySelector('.response-header-name').value.trim();
const value = row.querySelector('.response-header-value').value.trim();
if (name && value) {
headers[name] = value;
}
});
return headers;
}
// HLS response headers management
let hlsResponseHeaderCount = 0;
let hlsPropagateHeaderCount = 0;
function addHlsResponseHeader() {
const container = document.getElementById('hls-response-headers-container');
const headerId = `hls-response-header-${hlsResponseHeaderCount++}`;
const headerRow = document.createElement('div');
headerRow.id = headerId;
headerRow.className = 'flex gap-2 items-center';
headerRow.innerHTML = `
<input type="text" placeholder="Header Name (e.g., Cache-Control)" class="response-header-name flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<input type="text" placeholder="Header Value" class="response-header-value flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<button onclick="removeHlsResponseHeader('${headerId}')" class="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
`;
container.appendChild(headerRow);
}
function removeHlsResponseHeader(headerId) {
document.getElementById(headerId).remove();
}
function getHlsResponseHeaders() {
const headers = {};
document.querySelectorAll('#hls-response-headers-container > div').forEach(row => {
const name = row.querySelector('.response-header-name').value.trim();
const value = row.querySelector('.response-header-value').value.trim();
if (name && value) {
headers[name] = value;
}
});
return headers;
}
// HLS propagate headers management (rp_ prefix)
function addHlsPropagateHeader() {
const container = document.getElementById('hls-propagate-headers-container');
const headerId = `hls-propagate-header-${hlsPropagateHeaderCount++}`;
const headerRow = document.createElement('div');
headerRow.id = headerId;
headerRow.className = 'flex gap-2 items-center';
headerRow.innerHTML = `
<input type="text" placeholder="Header Name (e.g., content-type)" class="propagate-header-name flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<input type="text" placeholder="Header Value (e.g., video/mp2t)" class="propagate-header-value flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<button onclick="removeHlsPropagateHeader('${headerId}')" class="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
`;
container.appendChild(headerRow);
}
function removeHlsPropagateHeader(headerId) {
document.getElementById(headerId).remove();
}
function getHlsPropagateHeaders() {
const headers = {};
document.querySelectorAll('#hls-propagate-headers-container > div').forEach(row => {
const name = row.querySelector('.propagate-header-name').value.trim();
const value = row.querySelector('.propagate-header-value').value.trim();
if (name && value) {
headers[name] = value;
}
});
return headers;
}
// MPD response headers management
let mpdResponseHeaderCount = 0;
function addMpdResponseHeader() {
const container = document.getElementById('mpd-response-headers-container');
const headerId = `mpd-response-header-${mpdResponseHeaderCount++}`;
const headerRow = document.createElement('div');
headerRow.id = headerId;
headerRow.className = 'flex gap-2 items-center';
headerRow.innerHTML = `
<input type="text" placeholder="Header Name (e.g., Cache-Control)" class="response-header-name flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<input type="text" placeholder="Header Value" class="response-header-value flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<button onclick="removeMpdResponseHeader('${headerId}')" class="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
`;
container.appendChild(headerRow);
}
function removeMpdResponseHeader(headerId) {
document.getElementById(headerId).remove();
}
function getMpdResponseHeaders() {
const headers = {};
document.querySelectorAll('#mpd-response-headers-container > div').forEach(row => {
const name = row.querySelector('.response-header-name').value.trim();
const value = row.querySelector('.response-header-value').value.trim();
if (name && value) {
headers[name] = value;
}
});
return headers;
}
// Extractor request headers management
let extractorRequestHeaderCount = 0;
function addExtractorRequestHeader() {
const container = document.getElementById('extractor-request-headers-container');
const headerId = `extractor-request-header-${extractorRequestHeaderCount++}`;
const headerRow = document.createElement('div');
headerRow.id = headerId;
headerRow.className = 'flex gap-2 items-center';
headerRow.innerHTML = `
<input type="text" placeholder="Header Name (e.g., Referer)" class="request-header-name flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<input type="text" placeholder="Header Value" class="request-header-value flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<button onclick="removeExtractorRequestHeader('${headerId}')" class="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
`;
container.appendChild(headerRow);
}
function removeExtractorRequestHeader(headerId) {
document.getElementById(headerId).remove();
}
function getExtractorRequestHeaders() {
const headers = {};
document.querySelectorAll('#extractor-request-headers-container > div').forEach(row => {
const name = row.querySelector('.request-header-name').value.trim();
const value = row.querySelector('.request-header-value').value.trim();
if (name && value) {
headers[name] = value;
}
});
return headers;
}
// Extractor response headers management
let extractorResponseHeaderCount = 0;
function addExtractorResponseHeader() {
const container = document.getElementById('extractor-response-headers-container');
const headerId = `extractor-response-header-${extractorResponseHeaderCount++}`;
const headerRow = document.createElement('div');
headerRow.id = headerId;
headerRow.className = 'flex gap-2 items-center';
headerRow.innerHTML = `
<input type="text" placeholder="Header Name (e.g., Cache-Control)" class="response-header-name flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<input type="text" placeholder="Header Value" class="response-header-value flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-500 text-sm">
<button onclick="removeExtractorResponseHeader('${headerId}')" class="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
`;
container.appendChild(headerRow);
}
function removeExtractorResponseHeader(headerId) {
document.getElementById(headerId).remove();
}
function getExtractorResponseHeaders() {
const headers = {};
document.querySelectorAll('#extractor-response-headers-container > div').forEach(row => {
const name = row.querySelector('.response-header-name').value.trim();
const value = row.querySelector('.response-header-value').value.trim();
if (name && value) {
headers[name] = value;
}
});
return headers;
}
// Generate Proxy URL
function generateProxyUrl() {
const baseUrl = window.location.origin;
const apiPassword = document.getElementById('globalApiPassword').value.trim();
const hlsForm = document.getElementById('form-hls');
const mpdForm = document.getElementById('form-mpd');
const streamForm = document.getElementById('form-stream');
let url = '';
let params = new URLSearchParams();
if (!hlsForm.classList.contains('hidden')) {
const destination = document.getElementById('hls-destination').value.trim();
if (!destination) {
alert('Please enter a destination URL');
return;
}
url = `${baseUrl}/proxy/hls/manifest.m3u8`;
params.append('d', destination);
const keyUrl = document.getElementById('hls-key-url').value.trim();
if (keyUrl) params.append('key_url', keyUrl);
if (document.getElementById('hls-force-playlist').checked) params.append('force_playlist_proxy', 'true');
if (document.getElementById('hls-key-only').checked) params.append('key_only_proxy', 'true');
if (document.getElementById('hls-no-proxy').checked) params.append('no_proxy', 'true');
if (document.getElementById('hls-max-res').checked) params.append('max_res', 'true');
// Resolution selection
const resolution = document.getElementById('hls-resolution').value;
if (resolution) params.append('resolution', resolution);
// Skip segments
const skipSegments = document.getElementById('hls-skip-segments').value.trim();
if (skipSegments) params.append('skip', skipSegments);
// Start offset (for live streams)
const startOffset = document.getElementById('hls-start-offset').value.trim();
if (startOffset) params.append('start_offset', startOffset);
// Add HLS response headers (r_ prefix)
const hlsResponseHeaders = getHlsResponseHeaders();
for (const [name, value] of Object.entries(hlsResponseHeaders)) {
params.append(`r_${name}`, value);
}
// Add HLS propagate headers (rp_ prefix)
const hlsPropagateHeaders = getHlsPropagateHeaders();
for (const [name, value] of Object.entries(hlsPropagateHeaders)) {
params.append(`rp_${name}`, value);
}
// Add HLS remove headers (x_headers)
const hlsRemoveHeaders = document.getElementById('hls-remove-headers').value.trim();
if (hlsRemoveHeaders) {
params.append('x_headers', hlsRemoveHeaders);
}
} else if (!mpdForm.classList.contains('hidden')) {
const destination = document.getElementById('mpd-destination').value.trim();
if (!destination) {
alert('Please enter a destination URL');
return;
}
url = `${baseUrl}/proxy/mpd/manifest.m3u8`;
params.append('d', destination);
const keyId = document.getElementById('mpd-key-id').value.trim();
const key = document.getElementById('mpd-key').value.trim();
if (keyId) params.append('key_id', keyId);
if (key) params.append('key', key);
// Resolution selection
const resolution = document.getElementById('mpd-resolution').value;
if (resolution) params.append('resolution', resolution);
// Skip segments
const skipSegments = document.getElementById('mpd-skip-segments').value.trim();
if (skipSegments) params.append('skip', skipSegments);
// Start offset (for live streams)
const startOffset = document.getElementById('mpd-start-offset').value.trim();
if (startOffset) params.append('start_offset', startOffset);
// Add MPD response headers (r_ prefix)
const mpdResponseHeaders = getMpdResponseHeaders();
for (const [name, value] of Object.entries(mpdResponseHeaders)) {
params.append(`r_${name}`, value);
}
// Add MPD remove headers (x_headers)
const mpdRemoveHeaders = document.getElementById('mpd-remove-headers').value.trim();
if (mpdRemoveHeaders) {
params.append('x_headers', mpdRemoveHeaders);
}
} else if (!streamForm.classList.contains('hidden')) {
const destination = document.getElementById('stream-destination').value.trim();
if (!destination) {
alert('Please enter a destination URL');
return;
}
const isTranscode = document.getElementById('stream-transcode').checked;
const streamStartTime = document.getElementById('stream-start-time').value.trim();
const hasStreamStart = streamStartTime !== '' && parseFloat(streamStartTime) > 0;
if (isTranscode && !hasStreamStart) {
// Preferred transcode mode: HLS playlist URL for smooth playback/seeking.
url = `${baseUrl}/proxy/transcode/playlist.m3u8`;
params.append('d', destination);
} else if (isTranscode) {
// Legacy/direct transcode mode for explicit start offset support.
const filename = document.getElementById('stream-filename').value.trim();
if (filename) {
url = `${baseUrl}/proxy/stream/${encodeURIComponent(filename)}`;
} else {
url = `${baseUrl}/proxy/stream`;
}
params.append('d', destination);
params.append('transcode', 'true');
params.append('start', streamStartTime);
} else {
const filename = document.getElementById('stream-filename').value.trim();
if (filename) {
url = `${baseUrl}/proxy/stream/${encodeURIComponent(filename)}`;
} else {
url = `${baseUrl}/proxy/stream`;
}
params.append('d', destination);
}
// Add stream response headers (r_ prefix)
const responseHeaders = getStreamResponseHeaders();
for (const [name, value] of Object.entries(responseHeaders)) {
params.append(`r_${name}`, value);
}
// Add remove headers (x_headers)
const removeHeaders = document.getElementById('stream-remove-headers').value.trim();
if (removeHeaders) {
params.append('x_headers', removeHeaders);
}
}
// Add request headers
const headers = getProxyHeaders();
for (const [name, value] of Object.entries(headers)) {
params.append(`h_${name}`, value);
}
// Add API password
if (apiPassword) {
params.append('api_password', apiPassword);
}
const finalUrl = `${url}?${params.toString()}`;
document.getElementById('proxy-generated-url').textContent = finalUrl;
document.getElementById('proxy-output').classList.remove('hidden');
// Show/hide HLS preview button based on whether the URL is an HLS playlist
const previewBtn = document.getElementById('proxy-hls-preview-btn');
if (previewBtn) {
const generatedPath = new URL(finalUrl).pathname;
const isHlsPlaylist = generatedPath === '/proxy/transcode/playlist.m3u8';
if (isHlsPlaylist) {
previewBtn.classList.remove('hidden');
previewBtn.dataset.hlsUrl = finalUrl;
} else {
previewBtn.classList.add('hidden');
closeHlsPreview('proxy');
}
}
}
// Hosts that return HLS streams (auto-suggest .m3u8 extension)
const HLS_EXTRACTOR_HOSTS = [
'TurboVidPlay', 'FileMoon', 'StreamWish', 'VixCloud',
'LiveTV', 'LuluStream', 'DLHD', 'Fastream', 'Sportsonline',
'FileLions', 'Vidmoly', 'Voe'
];
// Handle extractor host selection change
function onExtractorHostChange() {
const host = document.getElementById('extractor-host').value;
const extensionSelect = document.getElementById('extractor-extension');
// Auto-suggest .m3u8 for HLS hosts if no extension is currently selected
if (HLS_EXTRACTOR_HOSTS.includes(host) && extensionSelect.value === '') {
extensionSelect.value = 'm3u8';
} else if (!HLS_EXTRACTOR_HOSTS.includes(host) && extensionSelect.value === 'm3u8') {
// Reset to none if switching to non-HLS host (only if m3u8 was auto-selected)
extensionSelect.value = '';
}
}
// Generate Extractor URL
function generateExtractorUrl() {
const baseUrl = window.location.origin;
const apiPassword = document.getElementById('globalApiPassword').value.trim();
const host = document.getElementById('extractor-host').value;
const destination = document.getElementById('extractor-destination').value.trim();
const extension = document.getElementById('extractor-extension').value;
if (!host) {
alert('Please select a video host');
return;
}
if (!destination) {
alert('Please enter a video URL');
return;
}
const params = new URLSearchParams();
params.append('host', host);
params.append('d', destination);
if (document.getElementById('extractor-redirect').checked) {
params.append('redirect_stream', 'true');
}
if (document.getElementById('extractor-max-res').checked) {
params.append('max_res', 'true');
}
if (document.getElementById('extractor-no-proxy').checked) {
params.append('no_proxy', 'true');
}
const extraParams = document.getElementById('extractor-extra-params').value.trim();
if (extraParams) {
try {
JSON.parse(extraParams);
params.append('extra_params', extraParams);
} catch (e) {
alert('Invalid JSON in extra parameters');
return;
}
}
// Add request headers (h_ prefix)
const requestHeaders = getExtractorRequestHeaders();
for (const [name, value] of Object.entries(requestHeaders)) {
params.append(`h_${name}`, value);
}
// Add response headers (r_ prefix)
const responseHeaders = getExtractorResponseHeaders();
for (const [name, value] of Object.entries(responseHeaders)) {
params.append(`r_${name}`, value);
}
// Add remove headers (x_headers)
const removeHeaders = document.getElementById('extractor-remove-headers').value.trim();
if (removeHeaders) {
params.append('x_headers', removeHeaders);
}
if (apiPassword) {
params.append('api_password', apiPassword);
}
// Build endpoint path with optional extension
const endpoint = extension ? `/extractor/video.${extension}` : '/extractor/video';
const finalUrl = `${baseUrl}${endpoint}?${params.toString()}`;
document.getElementById('extractor-generated-url').textContent = finalUrl;
document.getElementById('extractor-output').classList.remove('hidden');
}
// Generate Encoded URL
async function generateEncodedUrl() {
const proxyUrl = document.getElementById('encoded-proxy-url').value.trim();
const endpoint = document.getElementById('encoded-endpoint').value;
const destination = document.getElementById('encoded-destination').value.trim();
const apiPassword = document.getElementById('globalApiPassword').value.trim();
if (!proxyUrl) {
alert('Please enter the MediaFlow proxy URL');
return;
}
if (!endpoint) {
alert('Please select an endpoint');
return;
}
if (!destination) {
alert('Please enter a destination URL');
return;
}
// Build request body
const requestBody = {
mediaflow_proxy_url: proxyUrl,
endpoint: endpoint,
destination_url: destination,
query_params: {},
request_headers: {},
response_headers: {},
propagate_response_headers: {},
remove_response_headers: []
};
// Parse query params
const queryParamsStr = document.getElementById('encoded-query-params').value.trim();
if (queryParamsStr) {
try {
requestBody.query_params = JSON.parse(queryParamsStr);
} catch (e) {
alert('Invalid JSON in query parameters');
return;
}
}
// Parse request headers
const requestHeadersStr = document.getElementById('encoded-request-headers').value.trim();
if (requestHeadersStr) {
try {
requestBody.request_headers = JSON.parse(requestHeadersStr);
} catch (e) {
alert('Invalid JSON in request headers');
return;
}
}
// Parse response headers
const responseHeadersStr = document.getElementById('encoded-response-headers').value.trim();
if (responseHeadersStr) {
try {
requestBody.response_headers = JSON.parse(responseHeadersStr);
} catch (e) {
alert('Invalid JSON in response headers');
return;
}
}
// Parse propagate response headers
const propagateHeadersStr = document.getElementById('encoded-propagate-headers').value.trim();
if (propagateHeadersStr) {
try {
requestBody.propagate_response_headers = JSON.parse(propagateHeadersStr);
} catch (e) {
alert('Invalid JSON in propagate headers');
return;
}
}
// Parse remove response headers
const removeHeadersStr = document.getElementById('encoded-remove-headers').value.trim();
if (removeHeadersStr) {
requestBody.remove_response_headers = removeHeadersStr.split(',').map(h => h.trim()).filter(h => h);
}
// Add optional fields
const expiration = document.getElementById('encoded-expiration').value.trim();
if (expiration) requestBody.expiration = parseInt(expiration);
const ip = document.getElementById('encoded-ip').value.trim();
if (ip) requestBody.ip = ip;
const filename = document.getElementById('encoded-filename').value.trim();
if (filename) requestBody.filename = filename;
if (apiPassword) requestBody.api_password = apiPassword;
if (document.getElementById('encoded-base64').checked) {
requestBody.base64_encode_destination = true;
}
try {
const response = await fetch('/generate_url', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to generate URL');
}
const data = await response.json();
document.getElementById('encoded-generated-url').textContent = data.url;
document.getElementById('encoded-output').classList.remove('hidden');
} catch (error) {
alert('Error generating URL: ' + error.message);
}
}
// Copy URL to clipboard
function copyUrl(elementId, event) {
const url = document.getElementById(elementId).textContent;
const btn = event.currentTarget;
const originalText = btn.innerHTML;
navigator.clipboard.writeText(url).then(() => {
btn.innerHTML = '✓ Copied!';
setTimeout(() => {
btn.innerHTML = originalText;
}, 2000);
}).catch(err => {
alert('Failed to copy: ' + err);
});
}
// =================================================================
// HLS.js Preview Player
// =================================================================
// Track active HLS instances for cleanup
const _hlsInstances = {};
function openHlsPreview(prefix) {
const btn = document.getElementById(`${prefix}-hls-preview-btn`);
const playerDiv = document.getElementById(`${prefix}-hls-player`);
const video = document.getElementById(`${prefix}-video`);
if (!btn || !playerDiv || !video) return;
const hlsUrl = btn.dataset.hlsUrl;
if (!hlsUrl) return;
// Clean up previous instance
closeHlsPreview(prefix);
playerDiv.classList.remove('hidden');
if (Hls.isSupported()) {
const hls = new Hls({
maxBufferLength: 30,
maxMaxBufferLength: 60,
manifestLoadingMaxRetry: 2,
manifestLoadingRetryDelay: 2000,
levelLoadingMaxRetry: 2,
levelLoadingRetryDelay: 2000,
fragLoadingMaxRetry: 3,
fragLoadingRetryDelay: 2000,
});
let fatalRetries = 0;
const MAX_FATAL_RETRIES = 2;
hls.loadSource(hlsUrl);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function () {
fatalRetries = 0;
video.play().catch(() => {});
});
hls.on(Hls.Events.FRAG_LOADED, function () {
fatalRetries = 0; // reset on any successful load
});
hls.on(Hls.Events.ERROR, function (event, data) {
if (data.fatal) {
console.error('[HLS Preview] Fatal error:', data.type, data.details);
fatalRetries++;
if (fatalRetries > MAX_FATAL_RETRIES) {
console.warn('[HLS Preview] Max retries reached, stopping.');
hls.destroy();
delete _hlsInstances[prefix];
if (playerDiv) {
const video = playerDiv.querySelector('video');
if (video) {
video.replaceWith(Object.assign(document.createElement('p'), {
className: 'text-red-400 p-4 text-sm',
textContent: 'Playback failed \u2014 server may be unreachable. Close and try again later.',
}));
}
}
return;
}
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
setTimeout(() => hls.startLoad(), 3000);
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
hls.recoverMediaError();
} else {
hls.destroy();
delete _hlsInstances[prefix];
}
}
});
_hlsInstances[prefix] = hls;
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Native HLS support (Safari)
video.src = hlsUrl;
video.addEventListener('loadedmetadata', function () {
video.play().catch(() => {});
}, { once: true });
} else {
playerDiv.innerHTML = '<p class="text-red-400 p-4 text-sm">HLS playback is not supported in this browser.</p>';
}
}
function closeHlsPreview(prefix) {
const playerDiv = document.getElementById(`${prefix}-hls-player`);
const video = document.getElementById(`${prefix}-video`);
if (_hlsInstances[prefix]) {
_hlsInstances[prefix].destroy();
delete _hlsInstances[prefix];
}
if (video) {
video.pause();
video.removeAttribute('src');
video.load();
}
if (playerDiv) {
playerDiv.classList.add('hidden');
}
}
// XC Proxy functions
function base64UrlEncode(str) {
// Convert string to base64 and make it URL-safe
const base64 = btoa(str);
// Remove padding and replace + with - and / with _
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function generateXcConfig() {
const providerUrl = document.getElementById('xc-provider-url').value.trim();
const xcUsername = document.getElementById('xc-username').value.trim();
const xcPassword = document.getElementById('xc-password').value.trim();
const apiPassword = document.getElementById('globalApiPassword').value.trim();
if (!providerUrl) {
alert('Please enter your XC provider URL');
return;
}
if (!xcUsername) {
alert('Please enter your XC username');
return;
}
if (!xcPassword) {
alert('Please enter your XC password');
return;
}
// Build the combined string: {provider_url}:{xc_username}:{api_password}
// Then base64 encode the entire string
let combinedString;
if (apiPassword) {
combinedString = `${providerUrl}:${xcUsername}:${apiPassword}`;
} else {
combinedString = `${providerUrl}:${xcUsername}`;
}
// Base64 encode the entire combined string
const mediaflowUsername = base64UrlEncode(combinedString);
// Server URL is the current MediaFlow instance
const serverUrl = window.location.origin;
// Update the output fields
document.getElementById('xc-server-url').textContent = serverUrl;
document.getElementById('xc-generated-username').textContent = mediaflowUsername;
document.getElementById('xc-generated-password').textContent = xcPassword;
// Show the output section
document.getElementById('xc-output').classList.remove('hidden');
}
function copyXcField(elementId, event) {
const text = document.getElementById(elementId).textContent;
const btn = event.currentTarget;
const originalText = btn.innerHTML;
navigator.clipboard.writeText(text).then(() => {
btn.innerHTML = '✓';
setTimeout(() => {
btn.innerHTML = originalText;
}, 2000);
}).catch(err => {
alert('Failed to copy: ' + err);
});
}
function toggleCollapsible(header) {
const content = header.nextElementSibling;
const icon = header.querySelector('svg');
content.classList.toggle('open');
icon.classList.toggle('rotate-180');
}
// Acestream type switching (HLS/TS)
function selectAcestreamType(type) {
document.querySelectorAll('.acestream-type-btn').forEach(btn => {
btn.classList.remove('active-acestream-type', 'border-teal-500', 'bg-teal-50', 'dark:bg-teal-900/30', 'text-teal-700', 'dark:text-teal-300');
btn.classList.add('border-gray-200', 'dark:border-gray-600', 'bg-gray-50', 'dark:bg-gray-700/50', 'text-gray-700', 'dark:text-gray-300');
});
const activeBtn = document.getElementById(`acestream-type-${type}`);
activeBtn.classList.add('active-acestream-type', 'border-teal-500', 'bg-teal-50', 'dark:bg-teal-900/30', 'text-teal-700', 'dark:text-teal-300');
activeBtn.classList.remove('border-gray-200', 'dark:border-gray-600', 'bg-gray-50', 'dark:bg-gray-700/50', 'text-gray-700', 'dark:text-gray-300');
}
// Acestream ID type switching (content_id/infohash)
function selectAcestreamIdType(type) {
document.querySelectorAll('.acestream-id-type-btn').forEach(btn => {
btn.classList.remove('active-acestream-id-type', 'border-cyan-500', 'bg-cyan-50', 'dark:bg-cyan-900/30', 'text-cyan-700', 'dark:text-cyan-300');
btn.classList.add('border-gray-200', 'dark:border-gray-600', 'bg-gray-50', 'dark:bg-gray-700/50', 'text-gray-700', 'dark:text-gray-300');
});
const activeBtn = document.getElementById(`acestream-id-type-${type}`);
activeBtn.classList.add('active-acestream-id-type', 'border-cyan-500', 'bg-cyan-50', 'dark:bg-cyan-900/30', 'text-cyan-700', 'dark:text-cyan-300');
activeBtn.classList.remove('border-gray-200', 'dark:border-gray-600', 'bg-gray-50', 'dark:bg-gray-700/50', 'text-gray-700', 'dark:text-gray-300');
// Toggle input fields
if (type === 'content_id') {
document.getElementById('acestream-content-id-input').classList.remove('hidden');
document.getElementById('acestream-infohash-input').classList.add('hidden');
} else {
document.getElementById('acestream-content-id-input').classList.add('hidden');
document.getElementById('acestream-infohash-input').classList.remove('hidden');
}
}
// Generate Acestream URL
function generateAcestreamUrl() {
const baseUrl = window.location.origin;
const apiPassword = document.getElementById('globalApiPassword').value.trim();
// Determine output type (TS or HLS)
const isTs = document.getElementById('acestream-type-ts').classList.contains('active-acestream-type');
const endpoint = isTs ? '/proxy/acestream/stream' : '/proxy/acestream/manifest.m3u8';
// Determine ID type and get value
const isContentId = document.getElementById('acestream-id-type-content_id').classList.contains('active-acestream-id-type');
let idParam, idValue;
if (isContentId) {
idValue = document.getElementById('acestream-content-id').value.trim();
idParam = 'id';
if (!idValue) {
alert('Please enter a Content ID');
return;
}
} else {
idValue = document.getElementById('acestream-infohash').value.trim();
idParam = 'infohash';
if (!idValue) {
alert('Please enter an Infohash');
return;
}
// Validate infohash format (40 hex characters)
if (!/^[a-fA-F0-9]{40}$/.test(idValue)) {
alert('Infohash must be exactly 40 hexadecimal characters');
return;
}
}
// Build URL
const params = new URLSearchParams();
params.append(idParam, idValue);
// Transcode parameters
if (document.getElementById('acestream-transcode').checked) {
params.append('transcode', 'true');
const startTime = document.getElementById('acestream-start-time').value.trim();
if (startTime && parseFloat(startTime) > 0) {
params.append('start', startTime);
}
}
if (apiPassword) {
params.append('api_password', apiPassword);
}
// Start offset (for live streams) - only for HLS output
if (!isTs) {
const startOffset = document.getElementById('acestream-start-offset').value.trim();
if (startOffset) params.append('start_offset', startOffset);
}
const finalUrl = `${baseUrl}${endpoint}?${params.toString()}`;
document.getElementById('acestream-generated-url').textContent = finalUrl;
document.getElementById('acestream-output').classList.remove('hidden');
}
// =====================================================
// Telegram Session Generator Functions
// =====================================================
let currentSessionId = null;
let currentAuthType = 'phone';
function selectSessionAuthType(type) {
currentAuthType = type;
document.querySelectorAll('.session-auth-btn').forEach(btn => {
btn.classList.remove('active-session-auth', 'border-violet-500', 'bg-violet-50', 'dark:bg-violet-900/30', 'text-violet-700', 'dark:text-violet-300');
btn.classList.add('border-gray-200', 'dark:border-gray-600', 'bg-gray-50', 'dark:bg-gray-700/50', 'text-gray-700', 'dark:text-gray-300');
});
const activeBtn = document.getElementById(`session-auth-${type}`);
activeBtn.classList.add('active-session-auth', 'border-violet-500', 'bg-violet-50', 'dark:bg-violet-900/30', 'text-violet-700', 'dark:text-violet-300');
activeBtn.classList.remove('border-gray-200', 'dark:border-gray-600', 'bg-gray-50', 'dark:bg-gray-700/50', 'text-gray-700', 'dark:text-gray-300');
// Toggle input fields
if (type === 'phone') {
document.getElementById('session-phone-input').classList.remove('hidden');
document.getElementById('session-bot-input').classList.add('hidden');
document.getElementById('session-start-btn-text').textContent = 'Send Verification Code';
} else {
document.getElementById('session-phone-input').classList.add('hidden');
document.getElementById('session-bot-input').classList.remove('hidden');
document.getElementById('session-start-btn-text').textContent = 'Generate Session';
}
}
function showSessionError(message) {
const errorDiv = document.getElementById('session-error');
document.getElementById('session-error-message').textContent = message;
errorDiv.classList.remove('hidden');
}
function hideSessionError() {
document.getElementById('session-error').classList.add('hidden');
}
function setButtonLoading(btnId, loading) {
const textEl = document.getElementById(`${btnId}-text`);
const loadingEl = document.getElementById(`${btnId}-loading`);
const btn = document.getElementById(btnId);
if (loading) {
textEl.classList.add('hidden');
loadingEl.classList.remove('hidden');
btn.disabled = true;
} else {
textEl.classList.remove('hidden');
loadingEl.classList.add('hidden');
btn.disabled = false;
}
}
function showStep(stepNum) {
document.querySelectorAll('.session-step').forEach(step => step.classList.add('hidden'));
document.getElementById(`session-step-${stepNum}`).classList.remove('hidden');
}
function getSessionApiUrl(endpoint) {
const apiPassword = document.getElementById('globalApiPassword').value.trim();
let url = `/proxy/telegram/session/${endpoint}`;
if (apiPassword) {
url += `?api_password=${encodeURIComponent(apiPassword)}`;
}
return url;
}
async function startSessionGeneration() {
hideSessionError();
const apiId = document.getElementById('session-api-id').value.trim();
const apiHash = document.getElementById('session-api-hash').value.trim();
if (!apiId || !apiHash) {
showSessionError('Please enter both API ID and API Hash');
return;
}
if (!/^\d+$/.test(apiId)) {
showSessionError('API ID must be a number');
return;
}
const requestBody = {
api_id: parseInt(apiId),
api_hash: apiHash,
auth_type: currentAuthType
};
if (currentAuthType === 'phone') {
const phone = document.getElementById('session-phone').value.trim();
if (!phone) {
showSessionError('Please enter your phone number');
return;
}
requestBody.phone = phone;
} else {
const botToken = document.getElementById('session-bot-token').value.trim();
if (!botToken) {
showSessionError('Please enter your bot token');
return;
}
requestBody.bot_token = botToken;
}
setButtonLoading('session-start-btn', true);
try {
const response = await fetch(getSessionApiUrl('start'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || 'Failed to start session generation');
}
if (data.step === 'complete') {
// Bot auth completed immediately
showSessionResult(data);
} else if (data.step === 'code_sent') {
// Phone auth - code sent
currentSessionId = data.session_id;
showStep(2);
}
} catch (error) {
showSessionError(error.message);
} finally {
setButtonLoading('session-start-btn', false);
}
}
async function verifySessionCode() {
hideSessionError();
const code = document.getElementById('session-code').value.trim();
if (!code) {
showSessionError('Please enter the verification code');
return;
}
setButtonLoading('session-verify-btn', true);
try {
const response = await fetch(getSessionApiUrl('verify'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: currentSessionId,
code: code
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || 'Verification failed');
}
if (data.step === 'complete') {
showSessionResult(data);
} else if (data.step === '2fa_required') {
showStep('2fa');
}
} catch (error) {
showSessionError(error.message);
} finally {
setButtonLoading('session-verify-btn', false);
}
}
async function verify2FA() {
hideSessionError();
const password = document.getElementById('session-2fa-password').value;
if (!password) {
showSessionError('Please enter your 2FA password');
return;
}
setButtonLoading('session-2fa-btn', true);
try {
const response = await fetch(getSessionApiUrl('2fa'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: currentSessionId,
password: password
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || '2FA verification failed');
}
if (data.step === 'complete') {
showSessionResult(data);
}
} catch (error) {
showSessionError(error.message);
} finally {
setButtonLoading('session-2fa-btn', false);
}
}
function showSessionResult(data) {
document.getElementById('session-result-api-id').textContent = data.api_id;
document.getElementById('session-result-api-hash').textContent = data.api_hash;
document.getElementById('session-result-string').textContent = data.session_string;
showStep(3);
}
async function cancelSession() {
if (currentSessionId) {
try {
const apiPassword = document.getElementById('globalApiPassword').value.trim();
let url = `/proxy/telegram/session/cancel?session_id=${currentSessionId}`;
if (apiPassword) {
url += `&api_password=${encodeURIComponent(apiPassword)}`;
}
await fetch(url, {
method: 'POST'
});
} catch (e) {
// Ignore cancel errors
}
}
resetSessionGenerator();
}
function resetSessionGenerator() {
currentSessionId = null;
hideSessionError();
// Clear all inputs
document.getElementById('session-api-id').value = '';
document.getElementById('session-api-hash').value = '';
document.getElementById('session-phone').value = '';
document.getElementById('session-bot-token').value = '';
document.getElementById('session-code').value = '';
document.getElementById('session-2fa-password').value = '';
// Reset to phone auth
selectSessionAuthType('phone');
// Show step 1
showStep(1);
}
function copySessionConfig(event) {
const btn = event.currentTarget; // Capture button reference before async operation
const apiId = document.getElementById('session-result-api-id').textContent;
const apiHash = document.getElementById('session-result-api-hash').textContent;
const sessionString = document.getElementById('session-result-string').textContent;
const config = `ENABLE_TELEGRAM=true
TELEGRAM_API_ID=${apiId}
TELEGRAM_API_HASH=${apiHash}
TELEGRAM_SESSION_STRING=${sessionString}`;
const originalText = btn.innerHTML;
navigator.clipboard.writeText(config).then(() => {
btn.innerHTML = '✓ Copied!';
setTimeout(() => {
btn.innerHTML = originalText;
}, 2000);
}).catch(err => {
alert('Failed to copy: ' + err);
});
}
// =====================================================
// Telegram URL Generator
// =====================================================
let currentTelegramInputType = 'url';
function selectTelegramInputType(type) {
currentTelegramInputType = type;
document.querySelectorAll('.telegram-input-btn').forEach(btn => {
btn.classList.remove('active-telegram-input', 'border-sky-500', 'bg-sky-50', 'dark:bg-sky-900/30', 'text-sky-700', 'dark:text-sky-300');
btn.classList.add('border-gray-200', 'dark:border-gray-600', 'bg-gray-50', 'dark:bg-gray-700/50', 'text-gray-700', 'dark:text-gray-300');
});
const activeBtn = document.getElementById(`telegram-input-${type}`);
activeBtn.classList.add('active-telegram-input', 'border-sky-500', 'bg-sky-50', 'dark:bg-sky-900/30', 'text-sky-700', 'dark:text-sky-300');
activeBtn.classList.remove('border-gray-200', 'dark:border-gray-600', 'bg-gray-50', 'dark:bg-gray-700/50', 'text-gray-700', 'dark:text-gray-300');
// Toggle input fields - hide all first
document.getElementById('telegram-url-input').classList.add('hidden');
document.getElementById('telegram-ids-input').classList.add('hidden');
document.getElementById('telegram-fileid-input').classList.add('hidden');
// Show the selected one
if (type === 'url') {
document.getElementById('telegram-url-input').classList.remove('hidden');
} else if (type === 'ids') {
document.getElementById('telegram-ids-input').classList.remove('hidden');
} else if (type === 'file_id') {
document.getElementById('telegram-fileid-input').classList.remove('hidden');
}
}
function generateTelegramUrl() {
const baseUrl = window.location.origin;
const apiPassword = document.getElementById('globalApiPassword').value.trim();
const filename = document.getElementById('telegram-filename').value.trim();
const params = new URLSearchParams();
if (currentTelegramInputType === 'url') {
const telegramUrl = document.getElementById('telegram-url').value.trim();
if (!telegramUrl) {
alert('Please enter a Telegram URL');
return;
}
// Validate URL format
if (!telegramUrl.match(/^https?:\/\/t\.me\//)) {
alert('Invalid Telegram URL format. URL should start with https://t.me/');
return;
}
params.append('url', telegramUrl);
} else if (currentTelegramInputType === 'ids') {
const chatId = document.getElementById('telegram-chat-id').value.trim();
const messageId = document.getElementById('telegram-message-id').value.trim();
if (!chatId) {
alert('Please enter a Chat ID');
return;
}
if (!messageId) {
alert('Please enter a Message ID');
return;
}
params.append('chat_id', chatId);
params.append('message_id', messageId);
} else if (currentTelegramInputType === 'file_id') {
const fileId = document.getElementById('telegram-file-id').value.trim();
const fileSize = document.getElementById('telegram-file-size').value.trim();
if (!fileId) {
alert('Please enter a File ID');
return;
}
if (!fileSize) {
alert('Please enter the File Size (required for seeking/range requests)');
return;
}
params.append('file_id', fileId);
params.append('file_size', fileSize);
}
if (filename) {
params.append('filename', filename);
}
const isTranscode = document.getElementById('telegram-transcode').checked;
const telegramStartTime = document.getElementById('telegram-start-time').value.trim();
const hasTelegramStart = telegramStartTime !== '' && parseFloat(telegramStartTime) > 0;
if (apiPassword) {
params.append('api_password', apiPassword);
}
let finalUrl;
if (isTranscode && !hasTelegramStart) {
// Preferred transcode mode: HLS playlist URL for smooth playback/seeking.
finalUrl = `${baseUrl}/proxy/telegram/transcode/playlist.m3u8?${params.toString()}`;
} else {
if (isTranscode) {
params.append('transcode', 'true');
params.append('start', telegramStartTime);
}
finalUrl = `${baseUrl}/proxy/telegram/stream?${params.toString()}`;
}
document.getElementById('telegram-generated-url').textContent = finalUrl;
document.getElementById('telegram-output').classList.remove('hidden');
// Show/hide HLS preview button
const tgPreviewBtn = document.getElementById('telegram-hls-preview-btn');
if (tgPreviewBtn) {
if (isTranscode && !hasTelegramStart) {
tgPreviewBtn.classList.remove('hidden');
tgPreviewBtn.dataset.hlsUrl = finalUrl;
} else {
tgPreviewBtn.classList.add('hidden');
closeHlsPreview('telegram');
}
}
}
// Check Telegram Status
async function checkTelegramStatus() {
const baseUrl = window.location.origin;
const apiPassword = document.getElementById('globalApiPassword').value.trim();
const resultDiv = document.getElementById('telegram-status-result');
resultDiv.classList.remove('hidden');
resultDiv.innerHTML = '<div class="text-sm text-gray-500 dark:text-gray-400">Checking status...</div>';
try {
let url = `${baseUrl}/proxy/telegram/status`;
if (apiPassword) {
url += `?api_password=${encodeURIComponent(apiPassword)}`;
}
const response = await fetch(url);
const data = await response.json();
if (data.status === 'connected' || data.status === 'ready') {
resultDiv.innerHTML = `
<div class="p-3 bg-green-100 dark:bg-green-900/30 rounded-lg border border-green-200 dark:border-green-800">
<div class="flex items-center gap-2 text-green-700 dark:text-green-300 text-sm font-medium">
<span>✅</span>
<span>${data.status === 'connected' ? 'Connected' : 'Ready'}</span>
</div>
<p class="text-xs text-green-600 dark:text-green-400 mt-1">${data.message || 'Telegram proxy is ready to use'}</p>
</div>
`;
} else {
resultDiv.innerHTML = `
<div class="p-3 bg-red-100 dark:bg-red-900/30 rounded-lg border border-red-200 dark:border-red-800">
<div class="flex items-center gap-2 text-red-700 dark:text-red-300 text-sm font-medium">
<span>❌</span>
<span>${data.status === 'disabled' ? 'Disabled' : 'Not Connected'}</span>
</div>
<p class="text-xs text-red-600 dark:text-red-400 mt-1">${data.message || 'Telegram proxy is not configured or disabled'}</p>
</div>
`;
}
} catch (error) {
resultDiv.innerHTML = `
<div class="p-3 bg-red-100 dark:bg-red-900/30 rounded-lg border border-red-200 dark:border-red-800">
<div class="flex items-center gap-2 text-red-700 dark:text-red-300 text-sm font-medium">
<span>❌</span>
<span>Connection Failed</span>
</div>
<p class="text-xs text-red-600 dark:text-red-400 mt-1">${error.message}</p>
</div>
`;
}
}
// Check for hash on page load to switch to correct tab
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('encoded-proxy-url').value = window.location.origin;
// Check if URL has hash
if (window.location.hash === '#xc') {
switchTab('xc');
} else if (window.location.hash === '#acestream') {
switchTab('acestream');
} else if (window.location.hash === '#telegram') {
switchTab('telegram');
}
});
</script>
</body>
</html>