mirror of
https://github.com/UrloMythus/UnHided.git
synced 2026-04-11 11:50:51 +00:00
3052 lines
192 KiB
HTML
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>
|