有258人阅读过
一个M3U直播源合并和分组整理工具
发布于2025/07/23 更新于2025/07/23
[ 教程仅保证更新时有效,请自行测试。]
发布于2025/07/23 更新于2025/07/23
[ 教程仅保证更新时有效,请自行测试。]
[ 教程仅保证更新时有效,请自行测试。]
可以把多个M3U文件合并为一个,对分组名称进行修改,整理,输出为合并后的文件。
文件下载:
M3U.rar
文件是一个单一的html文件,可本地浏览器打开使用。
在线效果:M3U分组整理
网页代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>M3U分组整理</title>
<style>
:root {
--primary-color: #3498db;
--primary-hover: #2980b9;
--success-color: #2ecc71;
--success-hover: #27ae60;
--danger-color: #e74c3c;
--danger-hover: #c0392b;
--warning-color: #f39c12;
--warning-hover: #d35400;
--text-color: #333;
--light-text: #7f8c8d;
--bg-color: #f5f5f5;
--card-bg: #fff;
--border-color: #e0e0e0;
--highlight-bg: #f0f8ff;
--shadow: 0 4px 12px rgba(0,0,0,0.08);
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', system-ui, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
color: var(--text-color);
background-color: var(--bg-color);
}
h1 {
text-align: center;
color: #2c3e50;
margin-bottom: 30px;
font-weight: 600;
font-size: 2rem;
}
.container {
background-color: var(--card-bg);
padding: 30px;
border-radius: 12px;
box-shadow: var(--shadow);
transition: all 0.3s ease;
}
.section-title {
display: block;
margin: 25px 0 15px;
color: var(--primary-color);
font-weight: 600;
font-size: 1.1rem;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin: 25px 0 15px;
}
.section-title-with-button {
display: flex;
align-items: center;
color: var(--primary-color);
font-weight: 600;
font-size: 1.1rem;
}
textarea {
min-width: 90%;
height: 200px;
margin-bottom: 20px;
padding: 15px;
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 14px;
resize: vertical;
transition: border 0.3s;
font-family: Consolas, Monaco, monospace;
}
textarea:focus {
border-color: var(--primary-color);
outline: none;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
background-color: var(--primary-color);
color: white;
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 15px;
font-weight: 500;
margin-right: 12px;
margin-bottom: 12px;
transition: all 0.3s;
min-width: 120px;
line-height: 1.2;
}
.btn:hover {
background-color: var(--primary-hover);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.btn:active {
transform: translateY(0);
}
.btn i {
margin-right: 8px;
}
.btn-sm {
padding: 8px 15px;
font-size: 14px;
min-width: auto;
}
.btn-success {
background-color: var(--success-color);
}
.btn-success:hover {
background-color: var(--success-hover);
}
.btn-danger {
background-color: var(--danger-color);
}
.btn-danger:hover {
background-color: var(--danger-hover);
}
.btn-warning {
background-color: var(--warning-color);
}
.btn-warning:hover {
background-color: var(--warning-hover);
}
.editor-container {
margin-top: 30px;
display: none;
animation: fadeIn 0.5s ease;
}
.group-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
margin: 25px 0;
}
.group-item {
padding: 18px;
border: 1px solid var(--border-color);
border-radius: 8px;
background-color: #f9f9f9;
transition: all 0.3s;
}
.group-item:hover {
border-color: var(--primary-color);
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.group-item input {
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.group-item input:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
outline: none;
}
.original-value {
color: var(--light-text);
font-size: 13px;
margin-top: 8px;
font-style: italic;
}
.actions {
margin-top: 30px;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px;
}
#result {
margin-top: 30px;
padding: 25px;
background-color: var(--highlight-bg);
border: 1px solid #d6e9ff;
border-radius: 8px;
display: none;
font-family: Consolas, Monaco, monospace;
font-size: 13px;
line-height: 1.5;
word-break: break-all;
overflow-wrap: break-word;
}
#result pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
}
.stats {
margin: 15px;
padding: 15px;
background-color: #e8f4f8;
border-radius: 8px;
font-size: 15px;
text-align: center;
}
.stats strong {
color: var(--primary-color);
font-weight: 600;
}
.file-input-wrapper {
position: relative;
display: inline-block;
margin-bottom: 20px;
}
.file-input-wrapper input[type="file"] {
position: absolute;
left: 0;
top: 0;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.file-input-label {
display: inline-flex;
align-items: center;
padding: 12px 20px;
background-color: var(--warning-color);
color: white;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
}
.file-input-label:hover {
background-color: var(--warning-hover);
transform: translateY(-1px);
}
.file-input-label i {
margin-right: 8px;
}
.file-name {
margin-left: 15px;
color: var(--light-text);
font-size: 14px;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
.group-grid {
grid-template-columns: 1fr;
}
.actions {
flex-direction: column;
align-items: center;
}
.btn {
width: 100%;
margin-right: 0;
}
.section-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
}
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
</head>
<body>
<div>
<h1><i class="fas fa-tv"></i> M3U融合</h1>
<div>
<button>
<i class="fas fa-upload"></i> 选择文件(支持同时上传多个文件进行合并)
<span id="fileName">未选择文件</span>
</button>
<input type="file" id="fileInput" accept=".m3u,.txt" multiple>
</div>
<textarea id="m3uContent" placeholder="将M3U文件内容粘贴到这里..."></textarea>
<div style="text-align: center;">
<button id="parseBtn">
<i class="fas fa-search"></i> 解析分组标题
</button>
</div>
<div id="editorContainer">
<h2><i class="fas fa-edit"></i> 编辑分组标题</h2>
<div id="stats"></div>
<div id="groupsList"></div>
<div>
<button id="applyBtn" class="btn btn-success">
<i class="fas fa-check"></i> 应用更改
</button>
<button id="resetBtn" class="btn btn-danger">
<i class="fas fa-undo"></i> 重置所有修改
</button>
</div>
</div>
<div id="result"></div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const m3uContent = document.getElementById('m3uContent');
const parseBtn = document.getElementById('parseBtn');
const fileInput = document.getElementById('fileInput');
const fileName = document.getElementById('fileName');
const editorContainer = document.getElementById('editorContainer');
const groupsList = document.getElementById('groupsList');
const applyBtn = document.getElementById('applyBtn');
const resetBtn = document.getElementById('resetBtn');
const resultDiv = document.getElementById('result');
const statsDiv = document.getElementById('stats');
let originalContent = '';
let groupTitles = [];
fileInput.addEventListener('change', function () {
const files = Array.from(fileInput.files);
if (files.length === 0) {
fileName.textContent = '未选择文件';
return;
}
fileName.textContent = files.map(f => f.name).join(', ');
const readers = files.map(file => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = e => resolve(e.target.result);
reader.onerror = e => reject(e);
reader.readAsText(file, 'UTF-8');
});
});
Promise.all(readers)
.then(contents => {
const mergedContent = contents.join('\n\n');
m3uContent.value = mergedContent;
originalContent = mergedContent;
parseBtn.click(); // 自动解析
})
.catch(err => {
alert('读取文件失败:' + err.message);
});
});
parseBtn.addEventListener('click', function () {
originalContent = m3uContent.value.trim();
if (!originalContent) {
alert('请上传或粘贴 M3U 文件内容');
return;
}
groupTitles = extractGroupTitles(originalContent);
if (groupTitles.length === 0) {
alert('未找到任何 group-title');
return;
}
statsDiv.innerHTML = `共找到 <strong>${groupTitles.length}</strong> 个分组标题`;
displayGroupEditor(groupTitles);
editorContainer.style.display = 'block';
// 滚动到编辑器
editorContainer.scrollIntoView({ behavior: 'smooth' });
});
applyBtn.addEventListener('click', function () {
const modifiedContent = applyGroupTitleChanges(originalContent, groupTitles);
const changesCount = countChanges();
// 创建结果区域的HTML
const resultHTML = `
<div>
<div>
<i class="fas fa-file-alt"></i> 修改结果预览
</div>
<button id="resultDownloadBtn">
<i class="fas fa-download"></i> 下载M3U文件
</button>
</div>
<div>修改已完成,共更新了 <strong>${changesCount}</strong> 个分组标题</div>
<pre>${modifiedContent}</pre>
`;
resultDiv.innerHTML = resultHTML;
resultDiv.style.display = 'block';
m3uContent.value = modifiedContent;
// 为结果区域中的下载按钮添加事件监听
document.getElementById('resultDownloadBtn').addEventListener('click', function() {
const defaultName = fileInput.files[0] ?
fileInput.files[0].name.replace(/\.[^/.]+$/, "") + '_modified.m3u' :
'修改后的播放列表.m3u';
downloadFile(defaultName, modifiedContent);
});
// 滚动到结果
resultDiv.scrollIntoView({ behavior: 'smooth' });
});
resetBtn.addEventListener('click', function () {
if (confirm('确定要重置所有修改吗?所有编辑将被丢弃。')) {
displayGroupEditor(groupTitles);
resultDiv.style.display = 'none';
}
});
function extractGroupTitles(content) {
const lines = content.split('\n');
const groups = new Set();
for (let line of lines) {
line = line.trim();
if (line.startsWith('#EXTINF:-1')) {
const match = line.match(/group-title="([^"]+)"/);
if (match && match[1]) {
groups.add(match[1]);
}
}
}
return Array.from(groups).sort();
}
function displayGroupEditor(groups) {
groupsList.innerHTML = '';
groups.forEach(group => {
const groupItem = document.createElement('div');
groupItem.className = 'group-item';
const input = document.createElement('input');
input.type = 'text';
input.value = group;
input.dataset.original = group;
input.placeholder = '输入新的分组名称';
const originalSpan = document.createElement('div');
originalSpan.className = 'original-value';
originalSpan.textContent = `原始值: ${group}`;
groupItem.appendChild(input);
groupItem.appendChild(originalSpan);
groupsList.appendChild(groupItem);
});
}
function applyGroupTitleChanges(content, groups) {
let modified = content;
const inputs = groupsList.querySelectorAll('input');
inputs.forEach(input => {
const orig = input.dataset.original;
const curr = input.value.trim();
if (orig !== curr && curr !== '') {
const re = new RegExp(`group-title="${orig}"`, 'g');
modified = modified.replace(re, `group-title="${curr}"`);
}
});
// 清理所有以 #EXTM3U 开头的行,只保留第一个
const lines = modified.split('\n');
let extm3uFound = false;
const cleanedLines = [];
for (let line of lines) {
const trimmedLine = line.replace(/^\uFEFF/, '').trim();
if (/^#EXTM3U/i.test(trimmedLine)) {
if (!extm3uFound) {
cleanedLines.push(trimmedLine); // 保留第一个
extm3uFound = true;
}
// 跳过其他
} else {
cleanedLines.push(line);
}
}
return cleanedLines.join('\n');
}
function countChanges() {
let count = 0;
groupsList.querySelectorAll('input').forEach(input => {
if (input.value.trim() !== input.dataset.original && input.value.trim() !== '') {
count++;
}
});
return count;
}
function downloadFile(filename, content) {
const element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
});
</script>
</body>
</html>文章对你有帮助吗?
- 一般[0]

- 很赞[0]

- 没用[0]

- 垃圾[0]

- 无语[0]






