Free Tool - No account required

Free M3U & IPTV Playlist Validator

Check and validate your M3U, M3U8 or IPTV playlist online. Detects errors, missing headers, invalid URLs and duplicate channels instantly.

Drag & drop your M3U file here

or

.m3u and .m3u8 supported - Processed locally in your browser - Never uploaded to our servers

100% Private

Your file never leaves your browser. All processing runs locally - nothing is sent to our servers.

Instant Results

Validation completes in milliseconds, even for large playlists with tens of thousands of channels.

Detailed Report

Checks for missing #EXTM3U header, invalid URLs, missing group-title attributes and duplicate channels.

Want to do more with your playlist?

Create a free M3U Maker account to filter channels, create custom playlists and keep them auto-synced with your source file.

(function() { var zone = document.getElementById('uploadZone'); var fileInput = document.getElementById('fileInput'); var results = document.getElementById('results'); var resetBtn = document.getElementById('resetBtn'); var browseBtn = document.getElementById('browseBtn'); browseBtn.addEventListener('click', function(e) { e.stopPropagation(); fileInput.click(); }); zone.addEventListener('click', function() { fileInput.click(); }); zone.addEventListener('dragover', function(e) { e.preventDefault(); zone.classList.add('drag-over'); }); zone.addEventListener('dragleave', function() { zone.classList.remove('drag-over'); }); zone.addEventListener('drop', function(e) { e.preventDefault(); zone.classList.remove('drag-over'); if (e.dataTransfer.files[0]) processFile(e.dataTransfer.files[0]); }); fileInput.addEventListener('change', function() { if (fileInput.files[0]) processFile(fileInput.files[0]); }); resetBtn.addEventListener('click', function() { results.style.display = 'none'; loading.style.display = 'none'; zone.style.display = ''; fileInput.value = ''; }); var loading = document.getElementById('loading'); function processFile(file) { zone.style.display = 'none'; loading.style.display = ''; var reader = new FileReader(); reader.onload = function(e) { validateM3U(e.target.result, file.name); }; reader.readAsText(file, 'UTF-8'); } function validateM3U(content, filename) { var lines = content.split(/\r?\n/); var errors = [], warnings = []; var channelCount = 0; var urls = {}, names = {}; var i = 0; if (lines[0] && lines[0].trim().startsWith('#EXTM3U')) { i = 1; } else { errors.push({ line: 1, msg: 'Missing #EXTM3U header on line 1' }); } while (i < lines.length) { var line = lines[i].trim(); if (!line || line === '#EXTM3U') { i++; continue; } if (line.startsWith('#EXTINF:')) { channelCount++; var lineNum = i + 1; var nameMatch = line.match(/,(.+)$/); var name = nameMatch ? nameMatch[1].trim() : ''; if (!name) { warnings.push({ line: lineNum, msg: 'Channel #' + channelCount + ': missing name after comma in #EXTINF' }); } if (line.indexOf('group-title') === -1) { warnings.push({ line: lineNum, msg: 'Channel #' + channelCount + (name ? ' "' + name + '"' : '') + ': missing group-title attribute' }); } if (name) { var lname = name.toLowerCase(); if (names[lname] !== undefined) { warnings.push({ line: lineNum, msg: 'Duplicate channel name: "' + name + '" (also on line ' + names[lname] + ')' }); } else { names[lname] = lineNum; } } i++; while (i < lines.length && !lines[i].trim()) i++; var urlLine = lines[i] ? lines[i].trim() : ''; var urlLineNum = i + 1; if (!urlLine || urlLine.startsWith('#')) { errors.push({ line: lineNum, msg: 'Channel #' + channelCount + (name ? ' "' + name + '"' : '') + ': no stream URL found after #EXTINF' }); } else { if (!/^(https?|rtsp|rtp|udp|igmp|mms|rtmp):\/\//i.test(urlLine)) { warnings.push({ line: urlLineNum, msg: 'Channel #' + channelCount + (name ? ' "' + name + '"' : '') + ': unusual URL format' }); } if (urls[urlLine] !== undefined) { warnings.push({ line: urlLineNum, msg: 'Duplicate stream URL for "' + name + '" (also on line ' + urls[urlLine] + ')' }); } else { urls[urlLine] = urlLineNum; } } } i++; } showResults(filename, channelCount, errors, warnings); } function showResults(filename, channelCount, errors, warnings) { document.getElementById('resultsFilename').textContent = filename; var statsRow = document.getElementById('statsRow'); var dupeCount = warnings.filter(function(w) { return w.msg.indexOf('Duplicate') === 0; }).length; statsRow.textContent = ''; [ { val: channelCount.toLocaleString(), label: 'Channels', cls: '' }, { val: errors.length, label: 'Errors', cls: errors.length > 0 ? 'tool-stat--error' : '' }, { val: warnings.length, label: 'Warnings', cls: warnings.length > 0 ? 'tool-stat--warning' : '' }, { val: dupeCount, label: 'Duplicates', cls: '' } ].forEach(function(s) { var div = document.createElement('div'); div.className = 'tool-stat ' + s.cls; var val = document.createElement('span'); val.className = 'tool-stat-value'; val.textContent = s.val; var lbl = document.createElement('span'); lbl.className = 'tool-stat-label'; lbl.textContent = s.label; div.appendChild(val); div.appendChild(lbl); statsRow.appendChild(div); }); var total = errors.length * 3 + warnings.length; var score = Math.max(0, Math.min(100, Math.round(100 - (total / Math.max(channelCount, 1)) * 200))); var scoreColor = score >= 80 ? 'var(--accent-success)' : score >= 50 ? 'var(--accent-warning)' : 'var(--accent-danger)'; document.getElementById('scoreText').textContent = score + '/100 - ' + (score >= 80 ? 'Good' : score >= 50 ? 'Fair' : 'Poor'); var fill = document.getElementById('scoreFill'); fill.style.width = '0%'; setTimeout(function() { fill.style.width = score + '%'; fill.style.background = scoreColor; }, 50); var issueList = document.getElementById('issueList'); issueList.textContent = ''; function renderGroup(items, type) { if (!items.length) return; var group = document.createElement('div'); group.className = 'tool-issue-group'; var header = document.createElement('div'); header.className = 'tool-issue-group-header tool-issue--' + type; header.textContent = (type === 'error' ? 'Errors' : 'Warnings') + ' (' + items.length + ')'; group.appendChild(header); var list = document.createElement('div'); list.className = 'tool-issue-list'; items.slice(0, 50).forEach(function(issue) { var item = document.createElement('div'); item.className = 'tool-issue-item'; var lineSpan = document.createElement('span'); lineSpan.className = 'tool-issue-line'; lineSpan.textContent = 'L' + issue.line; var msgSpan = document.createElement('span'); msgSpan.className = 'tool-issue-msg'; msgSpan.textContent = issue.msg; item.appendChild(lineSpan); item.appendChild(msgSpan); list.appendChild(item); }); if (items.length > 50) { var more = document.createElement('div'); more.className = 'tool-issue-more'; more.textContent = '… and ' + (items.length - 50) + ' more ' + type + 's'; list.appendChild(more); } group.appendChild(list); issueList.appendChild(group); } renderGroup(errors, 'error'); renderGroup(warnings, 'warning'); document.getElementById('successMsg').style.display = (errors.length + warnings.length === 0) ? '' : 'none'; loading.style.display = 'none'; results.style.display = ''; } })();