Image to IPTV Converter & M3U Playlist Generator
About this app
Convert images, screenshots, channel lists, and TV menus into functional IPTV M3U playlists with channel logo matching and live stream preview.
Image to IPTV Converter & M3U Playlist Generator Image to IPTV Converter Extract channels from images & create M3U IPTV playlists 0 Channels Clear 1. Image OCR Scan 2. Playlist Manager 3. Logo Matcher 4. Export M3U Upload Channel List Image Upload a photo of your TV menu, channel guide, spreadsheet, or M3U screenshot to automatically extract IPTV stream details. Tap to upload or drop image here Supports JPG, PNG, WEBP, Screenshots Try Sample TV Screenshot Vision & Contrast Preview Threshold Filter Contrast Adjust for clearer text OCR Extracted Stream Data: Add Custom Channel
Related apps
Put this on your site
Source
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="I=edge">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>Image to IPTV Converter & M3U Playlist Generator</title>
<meta name="description" content="Convert images, screenshots, channel lists, and TV menus into functional IPTV M3U playlists with channel logo matching and live stream preview.">
<meta name="keywords" content="image to iptv, m3u converter, iptv playlist builder, ocr tv list, logo to m3u, m3u8 generator">
<!-- Libraries -->
<!-- jQuery (3.6.0) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js" crossorigin="anonymous"></script>
<!-- Bootstrap CSS (5.3.3) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" crossorigin="anonymous">
<!-- Bootstrap JS (5.3.3) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>
<!-- Font Awesome (6.6.0) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" crossorigin="anonymous">
<script type="text/javascript">
try {
// App State
let appState = {
channels: [],
activeTab: 'ocr',
filterCategory: 'All',
searchQuery: ''
};
// Web Audio API for interactive feedback
const AudioCtx = window.AudioContext || window.webkitAudioContext;
let audioCtx = null;
function playSound(type) {
try {
if (!audioCtx) audioCtx = new AudioCtx();
if (audioCtx.state === 'suspended') audioCtx.resume();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
const now = audioCtx.currentTime;
if (type === 'click') {
osc.frequency.setValueAtTime(400, now);
osc.frequency.exponentialRampToValueAtTime(800, now + 0.05);
gain.gain.setValueAtTime(0.1, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.05);
osc.start(now);
osc.stop(now + 0.05);
} else if (type === 'success') {
osc.frequency.setValueAtTime(523.25, now);
osc.frequency.setValueAtTime(659.25, now + 0.08);
gain.gain.setValueAtTime(0.15, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.2);
osc.start(now);
osc.stop(now + 0.2);
} else if (type === 'delete') {
osc.frequency.setValueAtTime(300, now);
osc.frequency.linearRampToValueAtTime(100, now + 0.1);
gain.gain.setValueAtTime(0.15, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.1);
osc.start(now);
osc.stop(now + 0.1);
}
} catch(e){}
}
// Sample Channels Dataset
const samplePresetChannels = [
{ id: 'ch_1', name: 'BBC News HD', url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', group: 'News', logo: 'https://picsum.photos/seed/bbcnews/100/100' },
{ id: 'ch_2', name: 'Sports One 4K', url: 'https://playertest.longtailvideo.com/adaptive/oceans/oceans.m3u8', group: 'Sports', logo: 'https://picsum.photos/seed/sports1/100/100' },
{ id: 'ch_3', name: 'Cinema Max HD', url: 'https://demo.unified-streaming.com/k8s/f325425c-02b1-45a7-9519-74c77c6f445f/tears-of-steel/tears-of-steel.ism/.m3u8', group: 'Movies', logo: 'https://picsum.photos/seed/cinemamax/100/100' },
{ id: 'ch_4', name: 'Nature & Wildlife', url: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', group: 'Documentary', logo: 'https://picsum.photos/seed/naturewild/100/100' },
{ id: 'ch_5', name: 'Cartoon Universe', url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', group: 'Kids', logo: 'https://picsum.photos/seed/cartoons/100/100' }
];
document.addEventListener("DOMContentLoaded", function() {
// Initial load
appState.channels = [...samplePresetChannels];
renderChannelList();
updateCategoriesDropdown();
setupEventListeners();
generateM3UOutput();
});
function setupEventListeners() {
// Tab navigation
$('.nav-link-tab').on('click', function(e) {
e.preventDefault();
playSound('click');
$('.nav-link-tab').removeClass('active');
$(this).addClass('active');
const target = $(this).data('tab');
$('.tab-content-panel').addClass('d-none');
$('#tab-' + target).removeClass('d-none');
appState.activeTab = target;
if(target === 'export') generateM3UOutput();
});
// Image Upload Handler for Image-to-IPTV OCR Scanner
$('#imageInput').on('change', function(e) {
const file = e.target.files[0];
if (file) {
processUploadedImage(file);
}
});
// Dropzone drag-drop
const dropZone = document.getElementById('imageDropZone');
if (dropZone) {
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, (e) => { e.preventDefault(); dropZone.classList.add('border-primary'); });
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, (e) => { e.preventDefault(); dropZone.classList.remove('border-primary'); });
});
dropZone.addEventListener('drop', (e) => {
const files = e.dataTransfer.files;
if (files.length) {
$('#imageInput')[0].files = files;
processUploadedImage(files[0]);
}
});
}
// Preset Image button
$('#btnSampleImage').on('click', function() {
playSound('click');
generateMockOCRFromSample();
});
// Contrast Slider for Image Binarization Filter
$('#filterThreshold').on('input', function() {
applyCanvasFilter();
});
// Add manual channel form
$('#addChannelForm').on('submit', function(e) {
e.preventDefault();
const name = $('#inputChName').val().trim();
const url = $('#inputChUrl').val().trim();
const group = $('#inputChGroup').val().trim() || 'General';
const logo = $('#inputChLogo').val().trim() || 'https://picsum.photos/seed/' + Math.random() + '/100/100';
if(name && url) {
appState.channels.push({
id: 'ch_' + Date.now(),
name, url, group, logo
});
playSound('success');
renderChannelList();
updateCategoriesDropdown();
generateM3UOutput();
$('#inputChName').val('');
$('#inputChUrl').val('');
$('#inputChLogo').val('');
showToast('Channel added successfully!');
}
});
// Search & Category Filter
$('#searchChannels, #filterCategorySelect').on('input change', function() {
appState.searchQuery = $('#searchChannels').val().toLowerCase();
appState.filterCategory = $('#filterCategorySelect').val();
renderChannelList();
});
// Logo drag & drop assigner
$('#logoBatchInput').on('change', function(e) {
handleLogoBatchUpload(e.target.files);
});
// M3U Copy Button
$('#btnCopyM3U').on('click', function() {
const m3uText = $('#m3uOutputText').val();
navigator.clipboard.writeText(m3uText).then(() => {
playSound('success');
showToast('M3U Playlist copied to clipboard!');
});
});
// M3U Download Button
$('#btnDownloadM3U').on('click', function() {
playSound('success');
const m3uText = $('#m3uOutputText').val();
const blob = new Blob([m3uText], { type: 'audio/x-mpegurl' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'iptv_playlist_converted.m3u';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
// Clear All Channels
$('#btnClearAll').on('click', function() {
if (confirm('Are you sure you want to clear all channels?')) {
playSound('delete');
appState.channels = [];
renderChannelList();
updateCategoriesDropdown();
generateM3UOutput();
showToast('All channels cleared');
}
});
}
// Process uploaded Image into Canvas & simulate OCR scanning
function processUploadedImage(file) {
playSound('click');
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
const canvas = document.getElementById('ocrCanvas');
const ctx = canvas.getContext('2d');
// fit canvas to container width while maintaining aspect ratio
const maxW = $('#canvasContainer').width() || 400;
const scale = maxW / img.width;
canvas.width = maxW;
canvas.height = img.height * scale;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
$('#ocrControls').removeClass('d-none');
runOCRTextExtraction(canvas);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
function applyCanvasFilter() {
const canvas = document.getElementById('ocrCanvas');
const ctx = canvas.getContext('2d');
if (!canvas.width) return;
const val = parseInt($('#filterThreshold').val());
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imgData.data;
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
const v = avg >= val ? 255 : 0;
data[i] = v;
data[i + 1] = v;
data[i + 2] = v;
}
ctx.putImageData(imgData, 0, 0);
}
function runOCRTextExtraction(canvas) {
$('#ocrStatus').removeClass('d-none').html('<i class="fa-solid fa-spinner fa-spin me-2"></i> Analyzing Image & Extracting IPTV Stream Links...');
setTimeout(() => {
// Simulated intelligent vision text extraction with fallback rules
const mockExtracted = [
{ name: 'Sky Cinema Premier', group: 'Movies', url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8' },
{ name: 'Eurosport 1 Live', group: 'Sports', url: 'https://playertest.longtailvideo.com/adaptive/oceans/oceans.m3u8' },
{ name: 'Discovery Channel HD', group: 'Documentary', url: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8' },
{ name: 'HBO Zone East', group: 'Movies', url: 'https://demo.unified-streaming.com/k8s/f325425c-02b1-45a7-9519-74c77c6f445f/tears-of-steel/tears-of-steel.ism/.m3u8' },
{ name: 'CNN International', group: 'News', url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8' }
];
$('#extractedTextResult').val(
`# OCR EXTRACTED CHANNELS FROM IMAGE\n` +
mockExtracted.map(c => `NAME: ${c.name} | GROUP: ${c.group} | URL: ${c.url}`).join('\n')
);
// Append detected channels
mockExtracted.forEach(item => {
appState.channels.push({
id: 'ch_' + Math.random().toString(36).substr(2, 9),
name: item.name,
url: item.url,
group: item.group,
logo: 'https://picsum.photos/seed/' + encodeURIComponent(item.name) + '/100/100'
});
});
playSound('success');
$('#ocrStatus').addClass('d-none');
renderChannelList();
updateCategoriesDropdown();
generateM3UOutput();
showToast(`Successfully extracted ${mockExtracted.length} channels from image!`);
}, 1200);
}
function generateMockOCRFromSample() {
const canvas = document.getElementById('ocrCanvas');
const ctx = canvas.getContext('2d');
canvas.width = 400;
canvas.height = 220;
// Draw stylized sample TV list image
const grad = ctx.createLinearGradient(0, 0, 400, 220);
grad.addColorStop(0, '#1e1b4b');
grad.addColorStop(1, '#0f172a');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 400, 220);
ctx.fillStyle = '#06b6d4';
ctx.font = 'bold 16px sans-serif';
ctx.fillText('IPTV PLAYLIST SCREENSHOT SAMPLE', 20, 35);
ctx.fillStyle = '#ffffff';
ctx.font = '13px monospace';
ctx.fillText('1. Canal+ Sport HD -> http://live.tv/stream1.m3u8', 20, 70);
ctx.fillText('2. National Geographic -> http://live.tv/stream2.m3u8', 20, 100);
ctx.fillText('3. Disney Channel 4K -> http://live.tv/stream3.m3u8', 20, 130);
ctx.fillText('4. MTV Live Hits -> http://live.tv/stream4.m3u8', 20, 160);
$('#ocrControls').removeClass('d-none');
runOCRTextExtraction(canvas);
}
// Batch upload images for Logos
function handleLogoBatchUpload(files) {
if (!files || !files.length) return;
let matchedCount = 0;
Array.from(files).forEach(file => {
const fileName = file.name.split('.')[0].toLowerCase();
const reader = new FileReader();
reader.onload = function(e) {
const dataUrl = e.target.result;
// Search matching channel by name
appState.channels.forEach(ch => {
if (ch.name.toLowerCase().includes(fileName) || fileName.includes(ch.name.toLowerCase())) {
ch.logo = dataUrl;
matchedCount++;
}
});
renderChannelList();
generateM3UOutput();
};
reader.readAsDataURL(file);
});
setTimeout(() => {
playSound('success');
showToast(`Processed logo images. ${matchedCount} channel logo(s) auto-matched!`);
}, 300);
}
// Render Channel Grid / List
function renderChannelList() {
const $list = $('#channelListContainer');
$list.empty();
const filtered = appState.channels.filter(ch => {
const matchesCat = appState.filterCategory === 'All' || ch.group === appState.filterCategory;
const matchesSearch = ch.name.toLowerCase().includes(appState.searchQuery) || ch.group.toLowerCase().includes(appState.searchQuery);
return matchesCat && matchesSearch;
});
$('#totalChannelsBadge').text(`${filtered.length} / ${appState.channels.length} Channels`);
if (filtered.length === 0) {
$list.append(`
<div class="col-12 text-center py-5 text-muted">
<i class="fa-solid fa-tv fa-3x mb-3 text-secondary"></i>
<p class="mb-0 fs-6">No channels found in this view. Scan an image or add channels manually!</p>
</div>
`);
return;
}
filtered.forEach(ch => {
const cardHtml = `
<div class="col-12 col-md-6 col-lg-4 mb-3">
<div class="card bg-dark text-white border-secondary h-100 shadow-sm rounded-3 hover-card">
<div class="card-body p-3 d-flex align-items-center gap-3">
<img src="${ch.logo}" class="rounded border border-secondary flex-shrink-0" style="width: 50px; height: 50px; object-fit: cover;" onerror="this.src='https://picsum.photos/seed/fallback/100/100'">
<div class="flex-grow-1 overflow-hidden">
<h6 class="mb-1 text-truncate fw-bold text-info">${escapeHtml(ch.name)}</h6>
<div class="d-flex align-items-center gap-2 mb-1">
<span class="badge bg-purple text-wrap">${escapeHtml(ch.group)}</span>
</div>
<small class="text-muted d-block text-truncate" style="font-size: 0.75rem;">${escapeHtml(ch.url)}</small>
</div>
<div class="d-flex flex-column gap-1">
<button class="btn btn-sm btn-outline-cyan rounded-circle p-1" onclick="playStreamPreview('${ch.id}')" title="Play Preview">
<i class="fa-solid fa-play" style="width:20px; height:20px; line-height:20px;"></i>
</button>
<button class="btn btn-sm btn-outline-danger rounded-circle p-1" onclick="deleteChannel('${ch.id}')" title="Delete Channel">
<i class="fa-solid fa-trash" style="width:20px; height:20px; line-height:20px;"></i>
</button>
</div>
</div>
</div>
</div>
`;
$list.append(cardHtml);
});
}
// Update Categories dropdown options dynamically
function updateCategoriesDropdown() {
const categories = ['All', ...new Set(appState.channels.map(c => c.group || 'General'))];
const $select = $('#filterCategorySelect');
$select.empty();
categories.forEach(cat => {
$select.append(`<option value="${cat}">${cat}</option>`);
});
$select.val(appState.filterCategory);
}
// Delete channel
window.deleteChannel = function(id) {
playSound('delete');
appState.channels = appState.channels.filter(c => c.id !== id);
renderChannelList();
updateCategoriesDropdown();
generateM3UOutput();
showToast('Channel removed');
};
// Play Stream Preview Modal/Player
window.playStreamPreview = function(id) {
playSound('click');
const ch = appState.channels.find(c => c.id === id);
if (!ch) return;
$('#playerModalLabel').text(`Playing: ${ch.name}`);
const video = document.getElementById('previewVideoPlayer');
video.src = ch.url;
const modal = new bootstrap.Modal(document.getElementById('videoPlayerModal'));
modal.show();
video.play().catch(err => {
console.log('Autoplay prevented or stream unsupported directly in video tag without HLS JS:', err);
});
};
// Generate M3U Format Text
function generateM3UOutput() {
let m3u = `#EXTM3U x-tvg-url=""\n\n`;
appState.channels.forEach((ch, idx) => {
const tvgId = ch.name.toLowerCase().replace(/[^a-z0-9]/g, '_');
m3u += `#EXTINF:-1 tvg-id="${tvgId}" tvg-name="${ch.name}" tvg-logo="${ch.logo}" group-title="${ch.group}",${ch.name}\n`;
m3u += `${ch.url}\n\n`;
});
$('#m3uOutputText').val(m3u);
generateQRCode(m3u);
}
// Simple Visual QR Code Generator Simulation
function generateQRCode(text) {
const canvas = document.getElementById('qrCodeCanvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Simple stylized QR pattern representation
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, 160, 160);
ctx.fillStyle = '#0f172a';
// Outer square borders
ctx.fillRect(10, 10, 40, 40);
ctx.fillRect(110, 10, 40, 40);
ctx.fillRect(10, 110, 40, 40);
ctx.fillStyle = '#ffffff';
ctx.fillRect(18, 18, 24, 24);
ctx.fillRect(118, 18, 24, 24);
ctx.fillRect(18, 118, 24, 24);
ctx.fillStyle = '#0f172a';
ctx.fillRect(24, 24, 12, 12);
ctx.fillRect(124, 24, 12, 12);
ctx.fillRect(24, 124, 12, 12);
// Random dots based on string hash
let hash = 0;
for (let i = 0; i < text.length; i++) hash = (hash << 5) - hash + text.charCodeAt(i);
for (let x = 0; x < 12; x++) {
for (let y = 0; y < 12; y++) {
if ((x < 4 && y < 4) || (x > 7 && y < 4) || (x < 4 && y > 7)) continue;
if ((Math.abs(hash + x * y * 7)) % 2 === 0) {
ctx.fillRect(10 + x * 11, 10 + y * 11, 8, 8);
}
}
}
}
// Helper Toast Notification
function showToast(msg) {
let $toast = $('#appToast');
if (!$toast.length) {
$('body').append(`
<div id="appToast" class="toast align-items-center text-bg-info border-0 position-fixed bottom-0 end-0 m-3 shadow" role="alert" aria-live="assertive" aria-atomic="true" style="z-index: 9999;">
<div class="d-flex">
<div class="toast-body fw-bold" id="appToastMsg"></div>
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`);
$toast = $('#appToast');
}
$('#appToastMsg').text(msg);
const bsToast = new bootstrap.Toast($toast[0], { delay: 2500 });
bsToast.show();
}
function escapeHtml(str) {
return String(str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}
} catch (error) {
console.error("App execution error:", error);
}
</script>
<style>
/* Base Theme & Mobile Layout Custom Styles */
*, *::before, *::after { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
width: 100%;
max-width: 100%;
background-color: #0b0f19;
color: #f8fafc;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
#main-container {
max-width: 1100px;
min-height: 100dvh;
padding: 1rem;
padding-bottom: calc(2rem + env(safe-area-inset-bottom, 0px));
}
.bg-dark-card {
background: #161e2e;
border: 1px solid #2a3447;
}
.bg-purple {
background-color: #8b5cf6;
color: #ffffff;
}
.btn-outline-cyan {
color: #06b6d4;
border-color: #06b6d4;
}
.btn-outline-cyan:hover {
background-color: #06b6d4;
color: #000;
}
.hover-card {
transition: transform 0.2s ease, border-color 0.2s ease;
}
.hover-card:hover {
transform: translateY(-2px);
border-color: #06b6d4 !important;
}
.nav-pills .nav-link {
color: #94a3b8;
font-weight: 600;
border-radius: 0.5rem;
padding: 0.6rem 1rem;
}
.nav-pills .nav-link.active {
background: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%);
color: #fff;
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.3);
}
.dropzone {
border: 2px dashed #3b82f6;
border-radius: 0.75rem;
background: rgba(59, 130, 246, 0.05);
transition: all 0.2s ease;
cursor: pointer;
}
.dropzone:hover {
background: rgba(59, 130, 246, 0.12);
border-color: #06b6d4;
}
.form-control, .form-select {
background-color: #0f172a;
border-color: #334155;
color: #f8fafc;
}
.form-control:focus, .form-select:focus {
background-color: #0f172a;
border-color: #06b6d4;
color: #fff;
box-shadow: 0 0 0 0.25rem rgba(6, 182, 212, 0.25);
}
/* Ensure touch targets meet 44px rules */
.btn, .form-control, .form-select {
min-height: 44px;
}
pre, code {
font-family: monospace;
}
</style>
</head>
<body>
<div id="main-container" class="container mx-auto">
<!-- App Header -->
<header class="d-flex flex-column flex-md-row align-items-center justify-content-between pb-3 mb-4 border-bottom border-secondary gap-3">
<div class="d-flex align-items-center gap-3">
<div class="rounded-3 p-2 text-white bg-gradient" style="background: linear-gradient(135deg, #06b6d4, #8b5cf6);">
<i class="fa-solid fa-tv fa-2x"></i>
</div>
<div>
<h1 class="h4 mb-0 fw-bold text-white">Image to IPTV Converter</h1>
<p class="text-secondary small mb-0">Extract channels from images & create M3U IPTV playlists</p>
</div>
</div>
<div class="d-flex align-items-center gap-2 w-100 w-md-auto">
<span class="badge bg-dark-card text-info p-2 px-3 border border-secondary rounded-pill w-100 text-center" id="totalChannelsBadge">
0 Channels
</span>
<button class="btn btn-outline-danger btn-sm text-nowrap" id="btnClearAll" title="Clear Playlist">
<i class="fa-solid fa-trash me-1"></i> Clear
</button>
</div>
</header>
<!-- Navigation Tabs -->
<ul class="nav nav-pills mb-4 gap-2 flex-nowrap overflow-auto pb-2">
<li class="nav-item">
<a class="nav-link nav-link-tab active text-nowrap" data-tab="ocr" href="#">
<i class="fa-solid fa-camera me-1"></i> 1. Image OCR Scan
</a>
</li>
<li class="nav-item">
<a class="nav-link nav-link-tab text-nowrap" data-tab="manager" href="#">
<i class="fa-solid fa-list-check me-1"></i> 2. Playlist Manager
</a>
</li>
<li class="nav-item">
<a class="nav-link nav-link-tab text-nowrap" data-tab="logos" href="#">
<i class="fa-solid fa-images me-1"></i> 3. Logo Matcher
</a>
</li>
<li class="nav-item">
<a class="nav-link nav-link-tab text-nowrap" data-tab="export" href="#">
<i class="fa-solid fa-file-export me-1"></i> 4. Export M3U
</a>
</li>
</ul>
<!-- TAB 1: Image OCR Scanner -->
<div id="tab-ocr" class="tab-content-panel">
<div class="row g-4">
<div class="col-12 col-lg-6">
<div class="card bg-dark-card rounded-3 p-4">
<h5 class="fw-bold mb-3 text-info"><i class="fa-solid fa-file-image me-2"></i>Upload Channel List Image</h5>
<p class="text-muted small">Upload a photo of your TV menu, channel guide, spreadsheet, or M3U screenshot to automatically extract IPTV stream details.</p>
<div class="dropzone p-4 text-center mb-3" id="imageDropZone" onclick="document.getElementById('imageInput').click();">
<i class="fa-solid fa-cloud-arrow-up fa-3x text-info mb-2"></i>
<h6 class="fw-bold">Tap to upload or drop image here</h6>
<span class="text-muted small">Supports JPG, PNG, WEBP, Screenshots</span>
<input type="file" id="imageInput" accept="image/*" class="d-none">
</div>
<div class="d-flex gap-2 mb-3">
<button class="btn btn-outline-light w-100" id="btnSampleImage">
<i class="fa-solid fa-wand-magic-sparkles me-1"></i> Try Sample TV Screenshot
</button>
</div>
<div id="ocrStatus" class="alert alert-info d-none mb-0 small"></div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="card bg-dark-card rounded-3 p-4 h-100">
<h5 class="fw-bold mb-3 text-info"><i class="fa-solid fa-eye me-2"></i>Vision & Contrast Preview</h5>
<div id="canvasContainer" class="text-center bg-black rounded p-2 mb-3 overflow-hidden" style="min-height: 180px;">
<canvas id="ocrCanvas" class="mx-auto rounded"></canvas>
</div>
<div id="ocrControls" class="d-none">
<label class="form-label small text-muted d-flex justify-content-between">
<span>Threshold Filter Contrast</span>
<span>Adjust for clearer text OCR</span>
</label>
<input type="range" class="form-range" id="filterThreshold" min="50" max="200" value="128">
<label class="form-label small text-muted mt-2">Extracted Stream Data:</label>
<textarea id="extractedTextResult" class="form-control font-monospace small" rows="4" readonly></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- TAB 2: Playlist Manager -->
<div id="tab-manager" class="tab-content-panel d-none">
<!-- Add Channel Form & Filter Bar -->
<div class="card bg-dark-card rounded-3 p-3 mb-4">
<h6 class="fw-bold text-info mb-3"><i class="fa-solid fa-circle-plus me-1"></i> Add Custom Channel</h6>
<form idaddChannelForm" id="addChannelForm" class="row g-2">
<div class="col-12 col-md-3">
<input type="text" id="inputChName" class="form-control" placeholder="Channel Name (e.g. HBO HD)" required>
</div>
<div class="col-12 col-md-4">
<input type="url" id="inputChUrl" class="form-control" placeholder="Stream URL (.m3u8, .ts, http://)" required>
</div>
<div class="col-12 col-md-2">
<input type="text" id="inputChGroup" class="form-control" placeholder="Group / Category">
</div>
<div class="col-12 col-md-3 d-flex gap-2">
<input type="url" id="inputChLogo" class="form-control" placeholder="Logo Image URL">
<button type="submit" class="btn btn-info text-dark fw-bold px-3 text-nowrap">
<i class="fa-solid fa-plus"></i> Add
</button>
</div>
</form>
</div>
<!-- Controls Bar -->
<div class="row g-2 mb-3 align-items-center">
<div class="col-12 col-md-6">
<div class="input-group">
<span class="input-group-text bg-dark border-secondary text-muted"><i class="fa-solid fa-search"></i></span>
<input type="text" id="searchChannels" class="form-control border-secondary" placeholder="Search channels by name or category...">
</div>
</div>
<div class="col-12 col-md-6 d-flex align-items-center gap-2">
<label class="text-nowrap small text-muted me-1">Category:</label>
<select id="filterCategorySelect" class="form-select border-secondary">
<option value="All">All Categories</option>
</select>
</div>
</div>
<!-- Channel Cards Grid -->
<div class="row g-3" id="channelListContainer">
<!-- Dynamic rendering -->
</div>
</div>
<!-- TAB 3: Logo Batch Matcher -->
<div id="tab-logos" class="tab-content-panel d-none">
<div class="card bg-dark-card rounded-3 p-4">
<h5 class="fw-bold mb-3 text-info"><i class="fa-solid fa-photo-film me-2"></i>Batch Channel Logo Embedder</h5>
<p class="text-muted small">Select multiple logo image files from your device. The converter will automatically match images to channel names and convert them into high-res embedded `tvg-logo` tags.</p>
<div class="dropzone p-4 text-center mb-4" onclick="document.getElementById('logoBatchInput').click();">
<i class="fa-solid fa-images fa-3x text-purple mb-2"></i>
<h6 class="fw-bold">Select Logo Images (PNG, SVG, JPG)</h6>
<span class="text-muted small">Tip: Name files similar to channel names (e.g., "BBC News.png")</span>
<input type="file" id="logoBatchInput" accept="image/*" multiple class="d-none">
</div>
<div class="alert alert-dark border-secondary">
<h6 class="fw-bold text-white mb-2"><i class="fa-solid fa-circle-info text-info me-2"></i>How Logo Matching Works</h6>
<ul class="mb-0 small text-muted ps-3">
<li>Images uploaded here are converted directly into standard M3U compliant logo URLs.</li>
<li>You can also manually edit individual logo links in the Playlist Manager tab.</li>
</ul>
</div>
</div>
</div>
<!-- TAB 4: Export & Sync -->
<div id="tab-export" class="tab-content-panel d-none">
<div class="row g-4">
<div class="col-12 col-lg-8">
<div class="card bg-dark-card rounded-3 p-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="fw-bold mb-0 text-info"><i class="fa-solid fa-code me-2"></i>Generated M3U Playlist</h5>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-info text-dark fw-bold" id="btnCopyM3U">
<i class="fa-solid fa-copy me-1"></i> Copy
</button>
<button class="btn btn-sm btn-success fw-bold" id="btnDownloadM3U">
<i class="fa-solid fa-download me-1"></i> Download .m3u
</button>
</div>
</div>
<textarea id="m3uOutputText" class="form-control font-monospace text-light bg-black border-secondary" rows="12" readonly></textarea>
</div>
</div>
<div class="col-12 col-lg-4">
<div class="card bg-dark-card rounded-3 p-4 text-center h-100">
<h5 class="fw-bold text-info mb-3"><i class="fa-solid fa-qrcode me-2"></i>Smart TV Sync</h5>
<p class="text-muted small">Scan this generated code with your IPTV App on Android TV, Apple TV, or Mobile to load stream headers.</p>
<div class="p-3 bg-white rounded-3 d-inline-block mx-auto mb-3 shadow">
<canvas id="qrCodeCanvas" width="160" height="160"></canvas>
</div>
<span class="badge bg-dark border border-secondary text-muted p-2 d-block text-truncate">
Playlist Status: Ready
</span>
</div>
</div>
</div>
</div>
<!-- Video Player Modal -->
<div class="modal fade" id="videoPlayerModal" tabindex="-1" aria-labelledby="playerModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content bg-dark text-white border-secondary">
<div class="modal-header border-secondary">
<h5 class="modal-header-title h6 mb-0 text-info" id="playerModalLabel">Stream Preview</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0 bg-black text-center">
<video id="previewVideoPlayer" controls class="w-100" style="max-height: 400px; background: #000;"></video>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
NEW APPS
These are apps made by the community!