(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 = '';
}
})();
We use cookiesEssential cookies keep the site running. We also use Google Analytics to understand how our site is used - only with your consent.
Cookie Policy