技术 M3U+TXT 转换 (源码)

iptv_320 · 2025年07月26日 · 133 次阅读

直接上传源码到你网站根目录

然后访问

http(s)://自己的域名/lives.php 即可

lives.php
<?php
require_once 'functions.php';

// 初始化变量
$txtToM3uResult = '';
$m3uToTxtResult = '';
$message = '';
$activeTab = isset($_GET['tab']) ? $_GET['tab'] : 'txt2m3u';

// 处理POST请求
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (isset($_POST['save_epg'])) {
        setcookie('epg_url', $_POST['epg_url'], time() + 86400 * 30);
        $message = 'EPG地址已保存';
    }

    if (isset($_POST['convert_txt_m3u'])) {
        $content = $_POST['txt_content'];
        $epgUrl = isset($_COOKIE['epg_url']) ? $_COOKIE['epg_url'] : 'https://epg.catvod.com/epg.xml';
        $rules = json_decode(isset($_COOKIE['rules']) ? $_COOKIE['rules'] : '[]', true);
        $txtToM3uResult = convertTXTToM3U($content, $epgUrl, $rules);
        $activeTab = 'txt2m3u';
    }

    if (isset($_POST['convert_m3u_txt'])) {
        $content = $_POST['m3u_content'];
        $m3uToTxtResult = convertM3UToTXT($content);
        $activeTab = 'm3u2txt';
    }

    if (isset($_POST['add_rule'])) {
        $keyword = $_POST['keyword'];
        $group = $_POST['group'];
        $rules = json_decode(isset($_COOKIE['rules']) ? $_COOKIE['rules'] : '[]', true);
        $rules[] = ['keyword' => $keyword, 'group' => $group];
        setcookie('rules', json_encode($rules), time() + 86400 * 30);
        $message = '规则已添加';
        $activeTab = 'config';
    }

    if (isset($_POST['delete_rule'])) {
        $index = $_POST['rule_index'];
        $rules = json_decode(isset($_COOKIE['rules']) ? $_COOKIE['rules'] : '[]', true);
        unset($rules[$index]);
        $rules = array_values($rules);
        setcookie('rules', json_encode($rules), time() + 86400 * 30);
        $message = '规则已删除';
        $activeTab = 'config';
    }
}

// 获取当前保存的EPG和规则
$currentEpg = isset($_COOKIE['epg_url']) ? $_COOKIE['epg_url'] : 'https://epg.catvod.com/epg.xml';
$currentRules = json_decode(isset($_COOKIE['rules']) ? $_COOKIE['rules'] : '[]', true);
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>在线 TXT/M3U 转换工具</title>
    <style>
        /* 基础样式 */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
        }

        body {
            background-color: #f8f9fa;
            color: #333;
            line-height: 1.6;
        }

        /* 蓝色顶栏 */
        .header {
            background-color: #007bff;
            color: white;
            padding: 15px 20px;
            font-weight: 600;
            font-size: 18px;
        }

        /* 页面容器 */
        .container {
            max-width: 1200px;
            margin: 0 auto;
        }

        /* 标签导航栏 */
        .tab-nav {
            display: flex;
            justify-content: center;
            background-color: #fff;
            border-bottom: 1px solid #dee2e6;
        }

        .tab-link {
            padding: 12px 20px;
            cursor: pointer;
            color: #495057;
            text-decoration: none;
            font-weight: 500;
            position: relative;
        }

        .tab-link.active {
            color: #007bff;
            border-bottom: 2px solid #007bff;
        }

        /* 内容区域 */
        .content-section {
            padding: 25px;
            background-color: white;
        }

        .tab-content {
            display: none;
            opacity: 0;
            transition: opacity 0.3s;
        }

        .tab-content.active {
            display: block;
            opacity: 1;
        }

        /* 蓝色边框标题 */
        .blue-title {
            border-left: 4px solid #007bff;
            padding-left: 12px;
            margin: 20px 0 15px 0;
            font-size: 16px;
            font-weight: 600;
            color: #495057;
        }

        /* 输入区域 */
        input[type="text"], 
        input[type="file"], 
        textarea {
            width: 100%;
            padding: 10px 12px;
            margin-bottom: 15px;
            border: 1px solid #ced4da;
            border-radius: 4px;
            font-size: 14px;
            transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
        }

        input[type="text"]:focus, 
        textarea:focus {
            border-color: #80bdff;
            outline: 0;
            box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
        }

        textarea {
            min-height: 150px;
            font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
            font-size: 13px;
        }

        /* 自定义文件输入 */
        .file-input-container {
            position: relative;
            margin-bottom: 15px;
        }

        .file-input-label {
            display: inline-block;
            padding: 8px 16px;
            background-color: #f8f9fa;
            border: 1px solid #ced4da;
            border-radius: 4px;
            cursor: pointer;
            color: #495057;
            transition: all 0.2s;
        }

        .file-input-label:hover {
            background-color: #e9ecef;
        }

        .file-input {
            position: absolute;
            left: -9999px;
        }

        .file-name {
            margin-left: 10px;
            color: #6c757d;
            font-size: 14px;
        }

        /* 按钮 */
        .btn {
            padding: 8px 16px;
            background-color: #f8f9fa;
            color: #212529;
            border: 1px solid #ced4da;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: all 0.2s;
            margin-right: 8px;
            margin-bottom: 8px;
        }

        .btn:hover {
            background-color: #e9ecef;
        }

        .btn-primary {
            background-color: #007bff;
            color: white;
            border-color: #007bff;
        }

        .btn-primary:hover {
            background-color: #0069d9;
            border-color: #0062cc;
        }

        .btn-danger {
            background-color: #dc3545;
            color: white;
            border-color: #dc3545;
        }

        .btn-danger:hover {
            background-color: #c82333;
            border-color: #bd2130;
        }

        .button-group {
            margin-bottom: 20px;
        }

        /* 搜索框 */
        .search-box {
            position: relative;
            margin-bottom: 20px;
        }

        .search-box input {
            padding-left: 30px;
        }

        .search-box:before {
            content: "🔍";
            position: absolute;
            left: 10px;
            top: 50%;
            transform: translateY(-50%);
            color: #6c757d;
            font-size: 14px;
        }

        /* 表格样式 */
        table {
            width: 100%;
            border-collapse: collapse;
            margin-bottom: 20px;
        }

        table, th, td {
            border: 1px solid #dee2e6;
        }

        th {
            background-color: #f8f9fa;
            padding: 10px;
            text-align: left;
            font-weight: 600;
            color: #495057;
        }

        td {
            padding: 10px;
            text-align: left;
        }

        tr:nth-child(even) {
            background-color: #f8f9fa;
        }

        tr:hover {
            background-color: #f1f3f5;
        }

        /* 结果区域 */
        .result-textarea {
            background-color: #f8f9fa;
            font-weight: normal;
        }

        /* 消息样式 */
        .message {
            padding: 12px 15px;
            margin-bottom: 20px;
            border-radius: 4px;
            background-color: #d4edda;
            border: 1px solid #c3e6cb;
            color: #155724;
            animation: fadeIn 0.5s;
        }

        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }

        /* 两列表单布局 */
        .form-row {
            display: flex;
            gap: 15px;
            margin-bottom: 15px;
        }

        .form-row > * {
            flex: 1;
            margin-bottom: 0;
        }

        /* 底部 */
        .footer {
            text-align: center;
            padding: 20px;
            color: #6c757d;
            font-size: 14px;
            margin-top: 10px;
            border-top: 1px solid #e9ecef;
        }
    </style>
</head>
<body>
    <div class="header">在线 TXT/M3U 转换工具</div>

    <div class="container">
        <div class="tab-nav">
            <a href="#config" class="tab-link <?php echo $activeTab == 'config' ? 'active' : ''; ?>" onclick="switchTab('config')">配置</a>
            <a href="#txt2m3u" class="tab-link <?php echo $activeTab == 'txt2m3u' ? 'active' : ''; ?>" onclick="switchTab('txt2m3u')">TXT 转 M3U</a>
            <a href="#m3u2txt" class="tab-link <?php echo $activeTab == 'm3u2txt' ? 'active' : ''; ?>" onclick="switchTab('m3u2txt')">M3U 转 TXT</a>
        </div>

        <div class="content-section">
            <?php if ($message): ?>
                <div class="message"><?php echo htmlspecialchars($message); ?></div>
            <?php endif; ?>

            <!-- 配置标签内容 -->
            <div id="config" class="tab-content <?php echo $activeTab == 'config' ? 'active' : ''; ?>">
                <div class="blue-title">EPG 地址</div>

                <form method="post">
                    <input type="text" name="epg_url" value="<?php echo htmlspecialchars($currentEpg); ?>" placeholder="例如: https://epg.catvod.com/epg.xml">
                    <button type="submit" name="save_epg" class="btn btn-primary">保存</button>
                </form>

                <div class="blue-title">分组规则</div>

                <form method="post">
                    <div class="form-row">
                        <input type="text" name="keyword" placeholder="关键词" required>
                        <input type="text" name="group" placeholder="分组名" required>
                    </div>
                    <button type="submit" name="add_rule" class="btn btn-primary">添加规则</button>
                </form>

                <?php if (!empty($currentRules)): ?>
                    <table>
                        <thead>
                            <tr>
                                <th>关键词</th>
                                <th>分组名</th>
                                <th>操作</th>
                            </tr>
                        </thead>
                        <tbody>
                            <?php foreach ($currentRules as $index => $rule): ?>
                            <tr>
                                <td><?php echo htmlspecialchars($rule['keyword']); ?></td>
                                <td><?php echo htmlspecialchars($rule['group']); ?></td>
                                <td>
                                    <form method="post" style="margin:0">
                                        <input type="hidden" name="rule_index" value="<?php echo $index; ?>">
                                        <button type="submit" name="delete_rule" class="btn btn-danger">删除</button>
                                    </form>
                                </td>
                            </tr>
                            <?php endforeach; ?>
                        </tbody>
                    </table>
                <?php else: ?>
                    <p style="color:#6c757d;margin-top:15px;">暂无分组规则,请添加规则。</p>
                <?php endif; ?>
            </div>

            <!-- TXT转M3U标签内容 -->
            <div id="txt2m3u" class="tab-content <?php echo $activeTab == 'txt2m3u' ? 'active' : ''; ?>">
                <form method="post" enctype="multipart/form-data">
                    <div class="file-input-container">
                        <label for="txt_file" class="file-input-label">选择文件</label>
                        <input type="file" id="txt_file" name="txt_file" accept=".txt" class="file-input" onchange="updateFileName(this, 'txt_filename')">
                        <span id="txt_filename" class="file-name">未选择任何文件</span>
                    </div>

                    <div class="blue-title">TXT 内容</div>
                    <textarea name="txt_content" id="txt_content" placeholder="频道名称,链接 或 分组名,#genre#"></textarea>

                    <div class="button-group">
                        <button type="submit" name="convert_txt_m3u" class="btn btn-primary">转换为 M3U</button>
                        <button type="button" onclick="clearForm('txt')" class="btn">清除</button>
                        <button type="button" onclick="copyResult('txt2m3u_result')" class="btn">复制结果</button>
                        <button type="button" onclick="saveFile('txt2m3u_result', 'm3u')" class="btn">保存文件</button>
                    </div>

                    <div class="search-box">
                        <input type="text" id="filter_txt" placeholder="输入关键字过滤频道" oninput="filterContent(this.value, 'txt_content')">
                    </div>

                    <?php if ($txtToM3uResult): ?>
                        <div class="blue-title">转换结果</div>
                        <textarea id="txt2m3u_result" class="result-textarea" readonly><?php echo htmlspecialchars($txtToM3uResult); ?></textarea>
                    <?php endif; ?>
                </form>
            </div>

            <!-- M3U转TXT标签内容 -->
            <div id="m3u2txt" class="tab-content <?php echo $activeTab == 'm3u2txt' ? 'active' : ''; ?>">
                <form method="post" enctype="multipart/form-data">
                    <div class="file-input-container">
                        <label for="m3u_file" class="file-input-label">选择文件</label>
                        <input type="file" id="m3u_file" name="m3u_file" accept=".m3u,.m3u8" class="file-input" onchange="updateFileName(this, 'm3u_filename')">
                        <span id="m3u_filename" class="file-name">未选择任何文件</span>
                    </div>

                    <div class="blue-title">M3U 内容</div>
                    <textarea name="m3u_content" id="m3u_content" placeholder="#EXTINF:-1 tvg-name=&quot;频道名称&quot; group-title=&quot;分组名称&quot;,频道名称"></textarea>

                    <div class="button-group">
                        <button type="submit" name="convert_m3u_txt" class="btn btn-primary">转换为 TXT</button>
                        <button type="button" onclick="clearForm('m3u')" class="btn">清除</button>
                        <button type="button" onclick="copyResult('m3u2txt_result')" class="btn">复制结果</button>
                        <button type="button" onclick="saveFile('m3u2txt_result', 'txt')" class="btn">保存文件</button>
                    </div>

                    <div class="search-box">
                        <input type="text" id="filter_m3u" placeholder="输入关键字过滤频道" oninput="filterContent(this.value, 'm3u_content')">
                    </div>

                    <?php if ($m3uToTxtResult): ?>
                        <div class="blue-title">转换结果</div>
                        <textarea id="m3u2txt_result" class="result-textarea" readonly><?php echo htmlspecialchars($m3uToTxtResult); ?></textarea>
                    <?php endif; ?>
                </form>
            </div>
        </div>

        <div class="footer">
            © 2025 Catvod.com - 所有权利保留
        </div>
    </div>

    <script>
        // 标签切换
        function switchTab(tabId) {
            // 隐藏所有标签内容
            document.querySelectorAll('.tab-content').forEach(function(tab) {
                tab.classList.remove('active');
            });

            // 取消所有标签激活状态
            document.querySelectorAll('.tab-link').forEach(function(link) {
                link.classList.remove('active');
            });

            // 激活选中的标签
            document.getElementById(tabId).classList.add('active');
            document.querySelector(`.tab-link[href="#${tabId}"]`).classList.add('active');

            // 阻止默认行为
            event.preventDefault();
        }

        // 文件上传预览
        document.addEventListener('DOMContentLoaded', function() {
            const txtFileInput = document.getElementById('txt_file');
            const m3uFileInput = document.getElementById('m3u_file');

            if (txtFileInput) {
                txtFileInput.addEventListener('change', function() {
                    const file = this.files[0];
                    if (file) {
                        const reader = new FileReader();
                        reader.onload = function(e) {
                            document.getElementById('txt_content').value = e.target.result;
                        };
                        reader.readAsText(file);
                    }
                });
            }

            if (m3uFileInput) {
                m3uFileInput.addEventListener('change', function() {
                    const file = this.files[0];
                    if (file) {
                        const reader = new FileReader();
                        reader.onload = function(e) {
                            document.getElementById('m3u_content').value = e.target.result;
                        };
                        reader.readAsText(file);
                    }
                });
            }

            // 如果页面加载后有消息,5秒后自动隐藏
            const messageBox = document.querySelector('.message');
            if (messageBox) {
                setTimeout(() => {
                    messageBox.style.display = 'none';
                }, 5000);
            }
        });

        // 更新文件名显示
        function updateFileName(fileInput, spanId) {
            const span = document.getElementById(spanId);
            if (fileInput.files.length > 0) {
                span.textContent = fileInput.files[0].name;
            } else {
                span.textContent = '未选择任何文件';
            }
        }

        // 清除表单内容
        function clearForm(type) {
            if (type === 'txt') {
                document.getElementById('txt_content').value = '';
                document.getElementById('txt_file').value = '';
                document.getElementById('txt_filename').textContent = '未选择任何文件';
                const resultArea = document.getElementById('txt2m3u_result');
                if (resultArea) resultArea.value = '';
            } else if (type === 'm3u') {
                document.getElementById('m3u_content').value = '';
                document.getElementById('m3u_file').value = '';
                document.getElementById('m3u_filename').textContent = '未选择任何文件';
                const resultArea = document.getElementById('m3u2txt_result');
                if (resultArea) resultArea.value = '';
            }
        }

        // 复制结果
        function copyResult(resultId) {
            const resultArea = document.getElementById(resultId);
            if (!resultArea || !resultArea.value) {
                alert('没有可复制的内容');
                return;
            }

            resultArea.select();
            document.execCommand('copy');

            // 显示临时提示
            const originalBg = resultArea.style.backgroundColor;
            resultArea.style.backgroundColor = '#d4edda';
            setTimeout(() => {
                resultArea.style.backgroundColor = originalBg;
            }, 300);

            alert('内容已复制到剪贴板');
        }

        // 保存为文件
        function saveFile(resultId, type) {
            const resultArea = document.getElementById(resultId);
            if (!resultArea || !resultArea.value) {
                alert('没有可保存的内容');
                return;
            }

            const blob = new Blob([resultArea.value], {type: 'text/plain'});
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.style.display = 'none';
            a.href = url;
            a.download = type === 'm3u' ? 'playlist.m3u' : 'channels.txt';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }

        // 过滤内容
        function filterContent(keyword, textareaId) {
            const textarea = document.getElementById(textareaId);

            if (!textarea || !textarea.value) return;

            if (!textarea.getAttribute('data-original')) {
                textarea.setAttribute('data-original', textarea.value);
            }

            const originalContent = textarea.getAttribute('data-original');

            if (!keyword) {
                textarea.value = originalContent;
                return;
            }

            const lines = originalContent.split('\n');
            const filteredLines = lines.filter(line => 
                line.toLowerCase().includes(keyword.toLowerCase())
            );

            textarea.value = filteredLines.join('\n');
        }
    </script>
</body>
</html>
functions.php


<?php
/**
 * 将TXT格式转换为M3U格式
 */
function convertTXTToM3U($content, $epgUrl = '', $rules = []) {
    $lines = explode("\n", $content);
    $m3u = "#EXTM3U\n";

    if (!empty($epgUrl)) {
        $m3u .= "#EXTM3U x-tvg-url=\"" . $epgUrl . "\"\n";
    }

    $currentGroup = "未分组";

    foreach ($lines as $line) {
        $line = trim($line);
        if (empty($line)) continue;

        // 处理分组标记行
        if (strpos($line, '#genre#') !== false) {
            $parts = explode(',', $line);
            if (count($parts) > 0) {
                $currentGroup = trim($parts[0]);
            }
            continue;
        }

        $parts = explode(',', $line, 2);
        if (count($parts) < 2) continue;

        $channelName = trim($parts[0]);
        $url = trim($parts[1]);

        // 应用分组规则
        $groupTitle = $currentGroup;
        if (!empty($rules)) {
            foreach ($rules as $rule) {
                if (stripos($channelName, $rule['keyword']) !== false) {
                    $groupTitle = $rule['group'];
                    break;
                }
            }
        }

        $m3u .= "#EXTINF:-1 tvg-name=\"" . $channelName . "\" group-title=\"" . $groupTitle . "\"," . $channelName . "\n";
        $m3u .= $url . "\n";
    }

    return $m3u;
}

/**
 * 将M3U格式转换为TXT格式
 */
function convertM3UToTXT($content) {
    $lines = explode("\n", $content);
    $txt = "";
    $currentChannel = "";
    $groups = array();

    // 第一次遍历,收集所有分组和频道
    for ($i = 0; $i < count($lines); $i++) {
        $line = trim($lines[$i]);
        if (empty($line) || $line == "#EXTM3U" || strpos($line, "#EXTM3U x-tvg-url=") === 0) continue;

        if (strpos($line, "#EXTINF") === 0) {
            // 获取分组信息
            preg_match('/group-title="([^"]+)"/', $line, $groupMatches);
            $groupName = isset($groupMatches[1]) ? $groupMatches[1] : "未分组";

            // 获取频道名称
            preg_match('/,(.*)$/', $line, $channelMatches);
            $channelName = isset($channelMatches[1]) ? trim($channelMatches[1]) : "";

            // 获取URL
            if ($i + 1 < count($lines)) {
                $url = trim($lines[$i + 1]);
                if (preg_match('/^(http|rtmp|rtsp)/', $url)) {
                    if (!isset($groups[$groupName])) {
                        $groups[$groupName] = array();
                    }

                    $groups[$groupName][] = array(
                        'name' => $channelName,
                        'url' => $url
                    );
                }
            }
        }
    }

    // 第二次处理,按分组生成TXT
    foreach ($groups as $groupName => $channels) {
        $txt .= $groupName . ",#genre#\n";

        foreach ($channels as $channel) {
            $txt .= $channel['name'] . "," . $channel['url'] . "\n";
        }

        $txt .= "\n"; // 每个分组后添加空行
    }

    return $txt;
}
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请 注册新账号