IPTV M3U8 & EPG Generator Studio | Image to M3U Playlist & Guide
About this app
IPTV M3U8 & EPG Generator Studio | Image to M3U Playlist & Guide StreamForge IPTV Studio Image to M3U8 & XMLTV EPG Online Generator Player & Channels EPG Schedule M3U / EPG Code Select a Channel HLS LIVE Image to Logo DataURI Converter Convert local channel logo images (PNG/JPG) directly into lightweight Base64 URLs embedded into your M3U playlist! Create Channel Channels Playlist Add Channel Add EPG Program Entry Select Target Channel Program Title Start Time End Time Category / Genre Description Add Program to EPG Auto-Fill 24h Schedule Channel Program Timeline Import Existing M3U Playlist Parse & Load M3U Generated .M3U8 Playlist Copy Download Generated XMLTV EPG (.xml) Copy Download Add IPTV Channel Channel Name * Group / Category TVG-ID (EPG Match) Stream M3U8 / Video URL * Logo URL or Base64 Image Cancel Save Channel
Related apps
Put this on your site
Source
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>IPTV M3U8 & EPG Generator Studio | Image to M3U Playlist & Guide</title>
<meta name="description" content="Create, convert, and manage IPTV M3U/M3U8 playlists with embedded image logos and XMLTV EPG schedules. Free online IPTV studio and stream player.">
<meta name="keywords" content="IPTV, M3U generator, M3U8 editor, EPG generator, XMLTV, Image to logo, IPTV player, HLS stream">
<!-- 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 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" crossorigin="anonymous">
<!-- HLS.js for web m3u8 playback -->
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script type="text/javascript">
try {
// App Javascript
document.addEventListener("DOMContentLoaded", function() {
// State Management
let channels = [
{
id: "ch_1",
tvgId: "news.world.hd",
name: "World News HD",
group: "News",
logo: "https://picsum.photos/id/1018/150/150",
url: "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8",
epg: [
{ title: "Global Morning Briefing", desc: "Live updates from around the globe.", start: "08:00", end: "10:00", category: "News" },
{ title: "Economy Today", desc: "Financial markets and market analysis.", start: "10:00", end: "12:00", category: "Business" },
{ title: "World Evening News", desc: "Prime time breaking coverage.", start: "18:00", end: "20:00", category: "News" }
]
},
{
id: "ch_2",
tvgId: "cinema.action.hd",
name: "Action Cinema 4K",
group: "Movies",
logo: "https://picsum.photos/id/1025/150/150",
url: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8",
epg: [
{ title: "Sintel Sci-Fi Feature", desc: "High octane action cinematic masterpiece.", start: "12:00", end: "14:30", category: "Action" },
{ title: "Cyber Cyberpunk 2099", desc: "Futuristic neon city thriller.", start: "14:30", end: "17:00", category: "Sci-Fi" }
]
},
{
id: "ch_3",
tvgId: "nature.wild.hd",
name: "Wild Planet Eco",
group: "Documentary",
logo: "https://picsum.photos/id/1024/150/150",
url: "https://test-streams.mux.dev/pts_shift/master.m3u8",
epg: [
{ title: "Deep Oceans Uncovered", desc: "Explore the deep sea marine ecosystems.", start: "09:00", end: "11:00", category: "Nature" },
{ title: "Savannah Predators", desc: "Wildlife documentary in East Africa.", start: "11:00", end: "13:00", category: "Documentary" }
]
}
];
let activeChannelId = channels[0].id;
let hlsPlayer = null;
// UI Elements
const channelListEl = $("#channelList");
const epgChannelSelect = $("#epgChannelSelect");
const epgListEl = $("#epgList");
const m3uPreviewEl = $("#m3uPreview");
const xmlEpgPreviewEl = $("#xmlEpgPreview");
const videoPlayer = document.getElementById("iptvVideoPlayer");
// Render Channel Cards / List
function renderChannels() {
channelListEl.empty();
epgChannelSelect.empty();
if (channels.length === 0) {
channelListEl.html(`<div class="text-center text-muted p-4"><i class="fa-solid fa-tv fa-2x mb-2"></i><p>No channels added yet. Click "Add Channel" or import an M3U file.</p></div>`);
return;
}
channels.forEach((ch, idx) => {
const isActive = ch.id === activeChannelId ? "active-channel" : "";
const itemHtml = `
<div class="channel-card card mb-2 ${isActive}" data-id="${ch.id}">
<div class="card-body p-2 d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-2 overflow-hidden" onclick="playChannel('${ch.id}')" style="cursor:pointer; flex: 1;">
<img src="${ch.logo}" class="channel-logo rounded" alt="logo" onerror="this.src='https://via.placeholder.com/48?text=TV'">
<div class="text-truncate">
<div class="fw-bold text-light text-truncate">${ch.name}</div>
<div class="small text-info text-truncate"><span class="badge bg-secondary me-1">${ch.group}</span> ID: ${ch.tvgId}</div>
</div>
</div>
<div class="d-flex align-items-center gap-1">
<button class="btn btn-sm btn-outline-warning" onclick="editChannel('${ch.id}')" title="Edit Channel"><i class="fa-solid fa-pen"></i></button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteChannel('${ch.id}')" title="Delete"><i class="fa-solid fa-trash"></i></button>
</div>
</div>
</div>
`;
channelListEl.append(itemHtml);
// Add to EPG select
epgChannelSelect.append(`<option value="${ch.id}">${ch.name} (${ch.group})</option>`);
});
if (epgChannelSelect.val()) {
renderEPGList(epgChannelSelect.val());
}
updateM3UOutput();
updateEPGOutput();
}
// Play Channel Stream
window.playChannel = function(id) {
activeChannelId = id;
$(".channel-card").removeClass("active-channel");
$(`.channel-card[data-id="${id}"]`).addClass("active-channel");
const ch = channels.find(c => c.id === id);
if (!ch) return;
$("#currentPlayingTitle").html(`<img src="${ch.logo}" class="rounded me-2" style="width:24px;height:24px;object-fit:cover;" onerror="this.src='https://via.placeholder.com/24?text=TV'">${ch.name}`);
// Destroy existing HLS instance
if (hlsPlayer) {
hlsPlayer.destroy();
hlsPlayer = null;
}
if (Hls.isSupported() && ch.url.includes(".m3u8")) {
hlsPlayer = new Hls();
hlsPlayer.loadSource(ch.url);
hlsPlayer.attachMedia(videoPlayer);
hlsPlayer.on(Hls.Events.MANIFEST_PARSED, function() {
videoPlayer.play().catch(e => console.log("Autoplay blocked"));
});
} else if (videoPlayer.canPlayType('application/vnd.apple.mpegurl')) {
// Safari native
videoPlayer.src = ch.url;
videoPlayer.addEventListener('loadedmetadata', function() {
videoPlayer.play();
});
} else {
// Fallback direct HTML5 video
videoPlayer.src = ch.url;
videoPlayer.play().catch(e => console.log("Direct play error: ", e));
}
};
// Delete Channel
window.deleteChannel = function(id) {
if (confirm("Are you sure you want to delete this channel?")) {
channels = channels.filter(c => c.id !== id);
if (activeChannelId === id && channels.length > 0) {
activeChannelId = channels[0].id;
}
renderChannels();
}
};
// Open Add / Edit Modal
window.openAddChannelModal = function() {
$("#channelModalTitle").text("Add New IPTV Channel");
$("#editChannelId").val("");
$("#channelNameInput").val("");
$("#channelGroupInput").val("General");
$("#channelTvgIdInput").val("tv." + Math.floor(Math.random()*1000));
$("#channelUrlInput").val("");
$("#channelLogoInput").val("");
$("#logoPreviewImg").attr("src", "https://via.placeholder.com/80?text=Logo").show();
$("#channelModal").modal("show");
};
window.editChannel = function(id) {
const ch = channels.find(c => c.id === id);
if (!ch) return;
$("#channelModalTitle").text("Edit Channel Details");
$("#editChannelId").val(ch.id);
$("#channelNameInput").val(ch.name);
$("#channelGroupInput").val(ch.group);
$("#channelTvgIdInput").val(ch.tvgId);
$("#channelUrlInput").val(ch.url);
$("#channelLogoInput").val(ch.logo);
$("#logoPreviewImg").attr("src", ch.logo).show();
$("#channelModal").modal("show");
};
// Save Channel From Modal
$("#saveChannelBtn").on("click", function() {
const id = $("#editChannelId").val();
const name = $("#channelNameInput").val().trim();
const group = $("#channelGroupInput").val().trim() || "General";
const tvgId = $("#channelTvgIdInput").val().trim() || "tv.channel";
const url = $("#channelUrlInput").val().trim();
const logo = $("#channelLogoInput").val().trim() || "https://via.placeholder.com/80?text=TV";
if (!name || !url) {
alert("Please fill in Channel Name and Stream M3U8 URL!");
return;
}
if (id) {
// Update existing
const ch = channels.find(c => c.id === id);
if (ch) {
ch.name = name;
ch.group = group;
ch.tvgId = tvgId;
ch.url = url;
ch.logo = logo;
}
} else {
// Add new
const newCh = {
id: "ch_" + Date.now(),
tvgId: tvgId,
name: name,
group: group,
logo: logo,
url: url,
epg: [
{ title: name + " Live Showcase", desc: "24/7 Continuous broadcast.", start: "00:00", end: "23:59", category: group }
]
};
channels.push(newCh);
activeChannelId = newCh.id;
}
$("#channelModal").modal("hide");
renderChannels();
});
// Image to Base64 Logo Converter
$("#imageFileInput").on("change", function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(event) {
const img = new Image();
img.onload = function() {
// Compress image to small thumbnail logo (128x128)
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = 128;
canvas.height = 128;
ctx.drawImage(img, 0, 0, 128, 128);
const compressedDataUrl = canvas.toDataURL("image/png", 0.8);
$("#channelLogoInput").val(compressedDataUrl);
$("#logoPreviewImg").attr("src", compressedDataUrl).show();
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
});
// Update Image Preview when typing Logo URL
$("#channelLogoInput").on("input", function() {
$("#logoPreviewImg").attr("src", $(this).val()).show();
});
// EPG Management Functions
function renderEPGList(channelId) {
epgListEl.empty();
const ch = channels.find(c => c.id === channelId);
if (!ch || !ch.epg || ch.epg.length === 0) {
epgListEl.html(`<div class="text-muted p-3 text-center">No program schedule added for this channel. Click "Add Show" or "Auto-Generate Schedule".</div>`);
return;
}
ch.epg.forEach((prog, index) => {
const progHtml = `
<div class="card bg-dark border-secondary mb-2 p-2">
<div class="d-flex justify-content-between align-items-start">
<div>
<span class="badge bg-primary me-2">${prog.start} - ${prog.end}</span>
<span class="badge bg-outline-info text-info me-2">${prog.category}</span>
<h6 class="d-inline text-light fw-bold">${prog.title}</h6>
<p class="small text-muted mb-0 mt-1">${prog.desc}</p>
</div>
<button class="btn btn-sm btn-link text-danger p-0 ms-2" onclick="deleteEpgItem('${ch.id}', ${index})"><i class="fa-solid fa-xmark"></i></button>
</div>
</div>
`;
epgListEl.append(progHtml);
});
}
$("#epgChannelSelect").on("change", function() {
renderEPGList($(this).val());
});
window.deleteEpgItem = function(channelId, index) {
const ch = channels.find(c => c.id === channelId);
if (ch && ch.epg) {
ch.epg.splice(index, 1);
renderEPGList(channelId);
updateEPGOutput();
}
};
// Add Program to EPG
$("#addEpgForm").on("submit", function(e) {
e.preventDefault();
const chId = $("#epgChannelSelect").val();
const ch = channels.find(c => c.id === chId);
if (!ch) return;
const title = $("#epgTitleInput").val().trim();
const desc = $("#epgDescInput").val().trim() || "No description provided.";
const start = $("#epgStartInput").val() || "12:00";
const end = $("#epgEndInput").val() || "13:00";
const category = $("#epgCategoryInput").val() || "General";
if (!title) return;
if (!ch.epg) ch.epg = [];
ch.epg.push({ title, desc, start, end, category });
$("#epgTitleInput").val("");
$("#epgDescInput").val("");
renderEPGList(chId);
updateEPGOutput();
});
// Auto Generate EPG Schedule
$("#autoGenerateEpgBtn").on("click", function() {
const chId = $("#epgChannelSelect").val();
const ch = channels.find(c => c.id === chId);
if (!ch) return;
const mockShows = [
{ title: ch.name + " Morning Talk", desc: "Live morning discussions & guest interviews.", category: "Talk Show" },
{ title: "Midday Express & World Highlights", desc: "Comprehensive continuous report.", category: "News" },
{ title: "Prime Time Blockbuster", desc: "Featured premium entertainment selection.", category: "Movies" },
{ title: "Nightbeat Live & Late Special", desc: "Late night entertainment and music.", category: "Entertainment" }
];
ch.epg = [
{ start: "06:00", end: "10:00", ...mockShows[0] },
{ start: "10:00", end: "15:00", ...mockShows[1] },
{ start: "15:00", end: "20:00", ...mockShows[2] },
{ start: "20:00", end: "23:59", ...mockShows[3] }
];
renderEPGList(chId);
updateEPGOutput();
});
// M3U Generator Logic
function updateM3UOutput() {
let m3uStr = `#EXTM3U x-tvg-url="epg.xml"\n\n`;
channels.forEach(ch => {
m3uStr += `#EXTINF:-1 tvg-id="${ch.tvgId}" tvg-name="${ch.name}" tvg-logo="${ch.logo}" group-title="${ch.group}",${ch.name}\n`;
m3uStr += `${ch.url}\n\n`;
});
m3uPreviewEl.val(m3uStr);
}
// XML EPG Generator Logic
function updateEPGOutput() {
const todayStr = new Date().toISOString().replace(/-/g, "").split("T")[0];
let xml = `<?xml version="1.0" encoding="UTF-8"?>\n<tv generator-info-name="StreamForge IPTV Generator">\n`;
// Add channels
channels.forEach(ch => {
xml += ` <channel id="${ch.tvgId}">\n`;
xml += ` <display-name>${escapeXml(ch.name)}</display-name>\n`;
xml += ` <icon src="${escapeXml(ch.logo)}" />\n`;
xml += ` </channel>\n`;
});
// Add programs
channels.forEach(ch => {
if (ch.epg) {
ch.epg.forEach(p => {
const startFormatted = todayStr + p.start.replace(":", "") + "00 +0000";
const endFormatted = todayStr + p.end.replace(":", "") + "00 +0000";
xml += ` <programme start="${startFormatted}" stop="${endFormatted}" channel="${ch.tvgId}">\n`;
xml += ` <title lang="en">${escapeXml(p.title)}</title>\n`;
xml += ` <desc lang="en">${escapeXml(p.desc)}</desc>\n`;
xml += ` <category lang="en">${escapeXml(p.category)}</category>\n`;
xml += ` </programme>\n`;
});
}
});
xml += `</tv>`;
xmlEpgPreviewEl.val(xml);
}
function escapeXml(unsafe) {
return unsafe.replace(/[<>&'"]/g, function (c) {
switch (c) {
case '<': return '<';
case '>': return '>';
case '&': return '&';
case '\'': return ''';
case '"': return '"';
}
});
}
// M3U Import Parser
$("#importM3uBtn").on("click", function() {
const text = $("#importM3uText").val().trim();
if (!text) {
alert("Please paste M3U text or upload an .m3u file first.");
return;
}
const lines = text.split("\n");
let parsedChannels = [];
let currentCh = null;
lines.forEach(line => {
line = line.trim();
if (line.startsWith("#EXTINF:")) {
currentCh = {
id: "ch_" + Date.now() + "_" + Math.floor(Math.random()*1000),
tvgId: getAttr(line, "tvg-id") || "tv." + Math.floor(Math.random()*1000),
name: line.split(",")[1] || "Imported Channel",
group: getAttr(line, "group-title") || "Imported",
logo: getAttr(line, "tvg-logo") || "https://via.placeholder.com/80?text=TV",
url: "",
epg: []
};
} else if (line.startsWith("http://") || line.startsWith("https://") || line.includes(".m3u8")) {
if (currentCh) {
currentCh.url = line;
parsedChannels.push(currentCh);
currentCh = null;
}
}
});
if (parsedChannels.length > 0) {
channels = parsedChannels;
activeChannelId = channels[0].id;
renderChannels();
playChannel(activeChannelId);
$("#importM3uText").val("");
alert(`Successfully imported ${parsedChannels.length} channels!`);
// Switch to player tab
$("#pills-player-tab").tab("show");
} else {
alert("Could not parse any valid channels from the input. Make sure it follows #EXTINF format.");
}
});
function getAttr(str, attrName) {
const regex = new RegExp(attrName + '="([^"]*)"', 'i');
const match = str.match(regex);
return match ? match[1] : "";
}
// M3U File Upload Reader
$("#m3uFileInput").on("change", function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(evt) {
$("#importM3uText").val(evt.target.result);
};
reader.readAsText(file);
});
// Download File Utility
function downloadFile(filename, text) {
const element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
$("#downloadM3uBtn").on("click", function() {
downloadFile("playlist.m3u8", m3uPreviewEl.val());
});
$("#copyM3uBtn").on("click", function() {
m3uPreviewEl.select();
document.execCommand("copy");
alert("M3U Playlist copied to clipboard!");
});
$("#downloadXmlBtn").on("click", function() {
downloadFile("epg.xml", xmlEpgPreviewEl.val());
});
$("#copyXmlBtn").on("click", function() {
xmlEpgPreviewEl.select();
document.execCommand("copy");
alert("XML EPG copied to clipboard!");
});
// Initial Render
renderChannels();
playChannel(activeChannelId);
});
} catch (error) {
console.error("App Error:", error);
}
</script>
<style>
/* Mobile-first Base Styling */
*, *::before, *::after { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
width: 100%;
max-width: 100%;
overflow-x: hidden;
background-color: #0d0e15;
color: #e2e8f0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
#main-container {
width: 100%;
max-width: 1200px;
min-height: 100dvh;
padding: 0.75rem;
margin: 0 auto;
}
/* Neon Dark Aesthetics */
.app-header {
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #4c1d95 100%);
border: 1px solid #4338ca;
border-radius: 12px;
padding: 1rem;
box-shadow: 0 8px 20px rgba(0,0,0,0.4);
}
.nav-pills .nav-link {
color: #a5b4fc;
border-radius: 8px;
padding: 0.5rem 0.8rem;
font-weight: 600;
font-size: 0.9rem;
}
.nav-pills .nav-link.active {
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
color: #fff;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
}
.card-dark {
background-color: #161822;
border: 1px solid #2d3248;
border-radius: 12px;
}
.channel-card {
background-color: #1a1d2d;
border: 1px solid #2d3248;
transition: all 0.2s ease-in-out;
}
.channel-card:hover {
border-color: #6366f1;
}
.active-channel {
border: 2px solid #6366f1 !important;
background-color: #23273e !important;
box-shadow: 0 0 10px rgba(99, 102, 241, 0.3);
}
.channel-logo {
width: 44px;
height: 44px;
object-fit: cover;
background-color: #0d0e15;
border: 1px solid #374151;
}
.video-container {
position: relative;
width: 100%;
background: #000;
border-radius: 12px;
overflow: hidden;
border: 1px solid #2d3248;
aspect-ratio: 16/9;
}
video {
width: 100%;
height: 100%;
object-fit: contain;
}
.form-control, .form-select {
background-color: #0f111a;
border: 1px solid #2d3248;
color: #f1f5f9;
border-radius: 8px;
}
.form-control:focus, .form-select:focus {
background-color: #0f111a;
border-color: #6366f1;
color: #fff;
box-shadow: 0 0 0 0.25rem rgba(99, 102, 241, 0.25);
}
.code-preview {
font-family: monospace;
font-size: 0.82rem;
background-color: #0a0b10;
color: #38bdf8;
}
.btn-neon {
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
color: #fff;
border: none;
font-weight: 600;
}
.btn-neon:hover {
background: linear-gradient(135deg, #4f46e5 0%, #4338ca 100%);
color: #fff;
}
/* Custom Scrollbars */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #0d0e15;
}
::-webkit-scrollbar-thumb {
background: #2d3248;
border-radius: 4px;
}
</style>
</head>
<body>
<div id="main-container">
<!-- Header Bar -->
<div class="app-header mb-3 d-flex flex-column flex-md-row justify-content-between align-items-center gap-2">
<div class="d-flex align-items-center gap-2">
<div class="p-2 bg-primary bg-opacity-20 rounded-3 text-info">
<i class="fa-solid fa-play-circle fa-2x"></i>
</div>
<div>
<h5 class="mb-0 fw-bold text-white">StreamForge IPTV Studio</h5>
<p class="small text-indigo-200 mb-0" style="color: #c7d2fe;">Image to M3U8 & XMLTV EPG Online Generator</p>
</div>
</div>
<!-- Navigation Tabs -->
<ul class="nav nav-pills gap-1 w-100 w-md-auto justify-content-center" id="pills-tab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="pills-player-tab" data-bs-toggle="pill" data-bs-target="#pills-player" type="button" role="tab"><i class="fa-solid fa-tv me-1"></i> Player & Channels</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-epg-tab" data-bs-toggle="pill" data-bs-target="#pills-epg" type="button" role="tab"><i class="fa-solid fa-calendar-days me-1"></i> EPG Schedule</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-export-tab" data-bs-toggle="pill" data-bs-target="#pills-export" type="button" role="tab"><i class="fa-solid fa-file-code me-1"></i> M3U / EPG Code</button>
</li>
</ul>
</div>
<!-- Main Tab Content -->
<div class="tab-content" id="pills-tabContent">
<!-- TAB 1: IPTV PLAYER & CHANNEL MANAGER -->
<div class="tab-pane fade show active" id="pills-player" role="tabpanel">
<div class="row g-3">
<!-- Left Panel: Live Stream Player & Quick Controls -->
<div class="col-lg-7">
<div class="card card-dark p-3 mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0 fw-bold text-info" id="currentPlayingTitle"><i class="fa-solid fa-circle-play me-2"></i>Select a Channel</h6>
<span class="badge bg-danger animate-pulse"><i class="fa-solid fa-signal me-1"></i> HLS LIVE</span>
</div>
<div class="video-container">
<video id="iptvVideoPlayer" controls autoplay muted playsinline></video>
</div>
</div>
<!-- Image Logo Studio Card -->
<div class="card card-dark p-3">
<h6 class="fw-bold text-warning mb-2"><i class="fa-solid fa-image me-2"></i>Image to Logo DataURI Converter</h6>
<p class="small text-muted mb-2">Convert local channel logo images (PNG/JPG) directly into lightweight Base64 URLs embedded into your M3U playlist!</p>
<div class="input-group input-group-sm">
<input type="file" class="form-control" id="imageFileInput" accept="image/*">
<button class="btn btn-outline-info" type="button" onclick="openAddChannelModal()"><i class="fa-solid fa-plus me-1"></i> Create Channel</button>
</div>
</div>
</div>
<!-- Right Panel: Channel Playlist Manager -->
<div class="col-lg-5">
<div class="card card-dark p-3 h-100">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-bold mb-0 text-white"><i class="fa-solid fa-list-ul me-2"></i>Channels Playlist</h6>
<button class="btn btn-sm btn-neon" onclick="openAddChannelModal()"><i class="fa-solid fa-plus me-1"></i> Add Channel</button>
</div>
<div id="channelList" class="overflow-y-auto pe-1" style="max-height: 480px;">
<!-- Dynamic Channels Rendered Here -->
</div>
</div>
</div>
</div>
</div>
<!-- TAB 2: EPG SCHEDULE EDITOR -->
<div class="tab-pane fade" id="pills-epg" role="tabpanel">
<div class="row g-3">
<!-- Channel Selector & Program Entry Form -->
<div class="col-lg-5">
<div class="card card-dark p-3">
<h6 class="fw-bold text-info mb-3"><i class="fa-solid fa-pen-to-square me-2"></i>Add EPG Program Entry</h6>
<div class="mb-3">
<label class="form-label small text-muted">Select Target Channel</label>
<select class="form-select" id="epgChannelSelect">
<!-- Options added dynamically -->
</select>
</div>
<form id="addEpgForm">
<div class="mb-2">
<label class="form-label small text-muted">Program Title</label>
<input type="text" class="form-control" id="epgTitleInput" placeholder="e.g. Evening News Live" required>
</div>
<div class="row g-2 mb-2">
<div class="col-6">
<label class="form-label small text-muted">Start Time</label>
<input type="time" class="form-control" id="epgStartInput" value="18:00" required>
</div>
<div class="col-6">
<label class="form-label small text-muted">End Time</label>
<input type="time" class="form-control" id="epgEndInput" value="19:00" required>
</div>
</div>
<div class="mb-2">
<label class="form-label small text-muted">Category / Genre</label>
<input type="text" class="form-control" id="epgCategoryInput" placeholder="News, Movie, Sports...">
</div>
<div class="mb-3">
<label class="form-label small text-muted">Description</label>
<textarea class="form-control" id="epgDescInput" rows="2" placeholder="Brief program synopsis..."></textarea>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-neon"><i class="fa-solid fa-plus me-1"></i> Add Program to EPG</button>
<button type="button" class="btn btn-outline-warning btn-sm" id="autoGenerateEpgBtn"><i class="fa-solid fa-wand-magic-sparkles me-1"></i> Auto-Fill 24h Schedule</button>
</div>
</form>
</div>
</div>
<!-- EPG Schedule Display List -->
<div class="col-lg-7">
<div class="card card-dark p-3">
<h6 class="fw-bold text-white mb-3"><i class="fa-solid fa-clock-rotate-left me-2"></i>Channel Program Timeline</h6>
<div id="epgList" class="overflow-y-auto" style="max-height: 480px;">
<!-- Dynamic EPG list rendered here -->
</div>
</div>
</div>
</div>
</div>
<!-- TAB 3: IMPORT / EXPORT CODE STUDIO -->
<div class="tab-pane fade" id="pills-export" role="tabpanel">
<div class="row g-3">
<!-- Import Section -->
<div class="col-lg-12">
<div class="card card-dark p-3 mb-3">
<h6 class="fw-bold text-info mb-2"><i class="fa-solid fa-file-import me-2"></i>Import Existing M3U Playlist</h6>
<div class="row g-2">
<div class="col-md-8">
<textarea class="form-control code-preview" id="importM3uText" rows="3" placeholder="Paste #EXTM3U playlist content here..."></textarea>
</div>
<div class="col-md-4 d-flex flex-column gap-2 justify-content-center">
<input type="file" id="m3uFileInput" class="form-control form-control-sm" accept=".m3u,.m3u8,.txt">
<button class="btn btn-primary" id="importM3uBtn"><i class="fa-solid fa-file-import me-1"></i> Parse & Load M3U</button>
</div>
</div>
</div>
</div>
<!-- Generated M3U Output -->
<div class="col-lg-6">
<div class="card card-dark p-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="fw-bold text-success mb-0"><i class="fa-solid fa-list-check me-2"></i>Generated .M3U8 Playlist</h6>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-info" id="copyM3uBtn"><i class="fa-solid fa-copy"></i> Copy</button>
<button class="btn btn-success" id="downloadM3uBtn"><i class="fa-solid fa-download"></i> Download</button>
</div>
</div>
<textarea class="form-control code-preview" id="m3uPreview" rows="12" readonly></textarea>
</div>
</div>
<!-- Generated XMLTV EPG Output -->
<div class="col-lg-6">
<div class="card card-dark p-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="fw-bold text-warning mb-0"><i class="fa-solid fa-xml me-2"></i>Generated XMLTV EPG (.xml)</h6>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-info" id="copyXmlBtn"><i class="fa-solid fa-copy"></i> Copy</button>
<button class="btn btn-warning" id="downloadXmlBtn"><i class="fa-solid fa-download"></i> Download</button>
</div>
</div>
<textarea class="form-control code-preview" id="xmlEpgPreview" rows="12" readonly></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- MODAL: ADD / EDIT CHANNEL -->
<div class="modal fade" id="channelModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content card-dark text-light border-secondary">
<div class="modal-header border-secondary">
<h6 class="modal-title fw-bold" id="channelModalTitle">Add IPTV Channel</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" id="editChannelId">
<div class="mb-2">
<label class="form-label small text-muted">Channel Name *</label>
<input type="text" class="form-control" id="channelNameInput" placeholder="e.g. HBO HD, Sky Sports">
</div>
<div class="row g-2 mb-2">
<div class="col-6">
<label class="form-label small text-muted">Group / Category</label>
<input type="text" class="form-control" id="channelGroupInput" placeholder="e.g. Movies, Sports">
</div>
<div class="col-6">
<label class="form-label small text-muted">TVG-ID (EPG Match)</label>
<input type="text" class="form-control" id="channelTvgIdInput" placeholder="e.g. hbo.hd.us">
</div>
</div>
<div class="mb-2">
<label class="form-label small text-muted">Stream M3U8 / Video URL *</label>
<input type="url" class="form-control" id="channelUrlInput" placeholder="https://domain.com/live/stream.m3u8">
</div>
<div class="mb-3">
<label class="form-label small text-muted">Logo URL or Base64 Image</label>
<div class="d-flex gap-2">
<input type="text" class="form-control" id="channelLogoInput" placeholder="https://logo-url.com/logo.png">
<img id="logoPreviewImg" src="" class="rounded border" style="width:40px;height:40px;object-fit:cover;display:none;">
</div>
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-neon btn-sm" id="saveChannelBtn">Save Channel</button>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
NEW APPS
These are apps made by the community!