// 页面加载时预加载数据,减少等待时间 document.addEventListener('DOMContentLoaded', function () { showModal('live', popup = false); showModal('channel', popup = false); showModal('update', popup = false); showVersionLog(doCheckUpdate = true); }); // 提交配置表单 document.getElementById('settingsForm').addEventListener('submit', function (event) { event.preventDefault(); // 阻止默认表单提交 const fields = ['update_config', 'gen_xml', 'include_future_only', 'ret_default', 'all_chs', 'db_type', 'mysql_host', 'mysql_dbname', 'mysql_username', 'mysql_password', 'gen_list_enable', 'check_update', 'token_range', 'user_agent_range', 'live_template_enable', 'live_fuzzy_match', 'live_url_comment', 'live_tvg_logo_enable', 'live_tvg_id_enable', 'live_tvg_name_enable']; // 创建隐藏字段并将其添加到表单 const form = this; fields.forEach(field => { const hiddenInput = document.createElement('input'); hiddenInput.type = 'hidden'; hiddenInput.name = field; hiddenInput.value = document.getElementById(field).value; form.appendChild(hiddenInput); }); // 获取表单数据 const formData = new FormData(form); // 执行 fetch 请求 fetch('manage.php', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { const { db_type_set, interval_time, start_time, end_time } = data; let message = '配置已更新

'; if (!db_type_set) { message += 'MySQL 启用失败
数据库已设为 SQLite

'; document.getElementById('db_type').value = 'sqlite'; updateMySQLFields(); } message += interval_time === 0 ? "已取消定时任务" : `已设置定时任务
开始时间:${start_time}
结束时间:${end_time}
间隔周期:${formatTime(interval_time)}`; showMessageModal(message); }) .catch(() => showMessageModal('发生错误,请重试。')); }); // 保存配置 function updateConfig() { document.getElementById('update_config').click(); } // 检查数据库状况 // 这个功能只是为了快速跳转到 PMA,用户可以自行修改路径测试。 function handleDbManagement() { if (document.getElementById('db_type').value === 'mysql') { var img = new Image(); var timeout = setTimeout(function () { img.onerror(); }, 1000); // 设置 1 秒超时 img.onload = function () { clearTimeout(timeout); // 清除超时 window.open('http://' + window.location.hostname + ':8080', '_blank'); }; img.onerror = function () { clearTimeout(timeout); // 清除超时 showMessageModal('无法访问 phpMyAdmin 8080 端口,请自行使用 MySQL 管理工具进行管理。'); }; img.src = 'http://' + window.location.hostname + ':8080/favicon.ico'; // 测试 8080 端口 return false; } return true; // 如果不是 MySQL,正常跳转 } // 退出登录 function logout() { // 清除所有cookies document.cookie.split(";").forEach(function (cookie) { var name = cookie.split("=")[0].trim(); document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/'; }); // 清除本地存储 sessionStorage.clear(); // 重定向到登录页面 window.location.href = 'manage.php'; } // Ctrl+S 保存设置 document.addEventListener("keydown", function (event) { if (event.ctrlKey && event.key === "s") { event.preventDefault(); // 阻止默认行为,如保存页面 setGenListAndUpdateConfig(); } }); // Ctrl+/ 设置(取消)注释 document.getElementById('xml_urls').addEventListener('keydown', handleKeydown); document.getElementById('sourceUrlTextarea').addEventListener('keydown', handleKeydown); function handleKeydown(event) { if (event.ctrlKey && event.key === '/') { event.preventDefault(); const textarea = this; const { selectionStart, selectionEnd, value } = textarea; const lines = value.split('\n'); // 计算当前选中的行 const startLine = value.slice(0, selectionStart).split('\n').length - 1; const endLine = value.slice(0, selectionEnd).split('\n').length - 1; // 判断选中的行是否都已注释 const allCommented = lines.slice(startLine, endLine + 1).every(line => line.trim().startsWith('#')); const newLines = lines.map((line, index) => { if (index >= startLine && index <= endLine) { return allCommented ? line.replace(/^#\s*/, '') : '# ' + line; } return line; }); // 更新 textarea 的内容 textarea.value = newLines.join('\n'); // 检查光标开始位置是否在行首 const startLineStartIndex = value.lastIndexOf('\n', selectionStart - 1) + 1; const isStartInLineStart = (selectionStart - startLineStartIndex < 2); // 检查光标结束位置是否在行首 const endLineStartIndex = value.lastIndexOf('\n', selectionEnd - 1) + 1; const isEndInLineStart = (selectionEnd - endLineStartIndex < 2); // 计算光标新的开始位置 const newSelectionStart = isStartInLineStart ? startLineStartIndex : selectionStart + newLines[startLine].length - lines[startLine].length; // 计算光标新的结束位置 const lengthDiff = newLines.join('').length - lines.join('').length; const endLineDiff = newLines[endLine].length - lines[endLine].length; const newSelectionEnd = isEndInLineStart ? (endLineDiff > 0 ? endLineStartIndex + lengthDiff : endLineStartIndex + lengthDiff - endLineDiff) : selectionEnd + lengthDiff; // 恢复光标位置 textarea.setSelectionRange(newSelectionStart, newSelectionEnd); } } // 格式化时间 function formatTime(seconds) { const formattedHours = String(Math.floor(seconds / 3600)); const formattedMinutes = String(Math.floor((seconds % 3600) / 60)); return `${formattedHours}小时${formattedMinutes}分钟`; } // 更新 MySQL 按钮状态 function updateMySQLFields() { var dbType = document.getElementById('db_type').value; var isSQLite = (dbType === 'sqlite'); document.getElementById('mysql_host').disabled = isSQLite; document.getElementById('mysql_dbname').disabled = isSQLite; document.getElementById('mysql_username').disabled = isSQLite; document.getElementById('mysql_password').disabled = isSQLite; } // 显示带消息的模态框 function showModalWithMessage(modalId, messageId = '', message = '') { const modal = document.getElementById(modalId); if (messageId) document.getElementById(messageId).innerHTML = message; modal.style.zIndex = zIndex++; modal.style.display = "block"; const closeBtn = modal.querySelector(".close"); closeBtn.onmousedown = () => modal.style.display = "none"; // 处理点击模态框外部关闭 const handleClickOutside = (event) => { if (event.target === modal) { modal.style.display = "none"; window.removeEventListener('mousedown', handleClickOutside); // 关闭后移除事件监听器 } }; window.addEventListener('mousedown', handleClickOutside); // 阻止点击模态框内部时关闭 modal.querySelector('.modal-content').addEventListener('mousedown', (e) => e.stopPropagation()); } // 显示消息模态框 function showMessageModal(message) { showModalWithMessage("messageModal", "messageModalMessage", message); } let zIndex = 100; // 显示模态框公共函数 function showModal(type, popup = true, data = '') { var modal, logSpan, logContent; switch (type) { case 'epg': modal = document.getElementById("epgModal"); fetchData("manage.php?get_epg_by_channel=true&channel=" + encodeURIComponent(data.channel) + "&date=" + data.date, updateEpgContent); // 更新日期的点击事件 const updateDate = function (offset) { const currentDate = new Date(document.getElementById("epgDate").innerText); currentDate.setDate(currentDate.getDate() + offset); const newDateString = currentDate.toISOString().split('T')[0]; fetchData(`manage.php?get_epg_by_channel=true&channel=${encodeURIComponent(data.channel)}&date=${newDateString}`, updateEpgContent); document.getElementById("epgDate").innerText = newDateString; }; // 前一天和后一天的点击事件 document.getElementById('prevDate').onclick = () => updateDate(-1); document.getElementById('nextDate').onclick = () => updateDate(1); break; case 'update': modal = document.getElementById("updatelogModal"); fetchData('manage.php?get_update_logs=true', updateLogTable); break; case 'cron': modal = document.getElementById("cronlogModal"); fetchData('manage.php?get_cron_logs=true', updateCronLogContent); break; case 'channel': modal = document.getElementById("channelModal"); fetchData('manage.php?get_channel=true', updateChannelList); break; case 'icon': modal = document.getElementById("iconModal"); fetchData('manage.php?get_icon=true', updateIconList); break; case 'allicon': modal = document.getElementById("iconModal"); fetchData('manage.php?get_icon=true&get_all_icon=true', updateIconList); break; case 'channelbindepg': modal = document.getElementById("channelBindEPGModal"); fetchData('manage.php?get_channel_bind_epg=true', updateChannelBindEPGList); break; case 'channelmatch': modal = document.getElementById("channelMatchModal"); fetchData('manage.php?get_channel_match=true', updateChannelMatchList); break; case 'live': modal = document.getElementById("liveSourceManageModal"); fetchData('manage.php?get_live_data=true', updateLiveSourceModal); break; case 'moresetting': updateMySQLFields(); // 设置 MySQL 相关输入框状态 document.getElementById('db_type').addEventListener('change', updateMySQLFields); modal = document.getElementById("moreSettingModal"); fetchData('manage.php?get_gen_list=true', updateGenList); break; default: console.error('Unknown type:', type); break; } if (!popup) { return; } modal.style.zIndex = zIndex++; // 确保 modal 在最上层 modal.style.display = "block"; var originalOnMouseDown = window.onmousedown; function handleModalClose() { modal.style.display = "none"; window.onmousedown = originalOnMouseDown; // 恢复原事件 } closeBtn = modal.querySelector(".close"); closeBtn.onmousedown = handleModalClose; window.onmousedown = function (event) { if (event.target === modal) { handleModalClose(); } } } function fetchData(endpoint, callback) { fetch(endpoint) .then(response => response.json()) .then(data => callback(data)) .catch(error => { console.error('Error fetching log:', error); callback([]); }); } // 显示 update.php、check.php 执行结果 function showExecResult(fileName, callback, fullSize = true) { showMessageModal(''); const messageContainer = document.getElementById('messageModalMessage'); // 清空 messageContainer,避免内容重复 messageContainer.innerHTML = ''; const wrapper = document.createElement('div'); if (fullSize) { wrapper.style.width = '800px'; wrapper.style.height = '500px'; } wrapper.style.overflow = 'auto'; messageContainer.appendChild(wrapper); // 创建 XMLHttpRequest 对象 const xhr = new XMLHttpRequest(); xhr.open('GET', `${fileName}`, true); // 显式设置 X-Requested-With 请求头 xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); // 处理接收到的数据 xhr.onprogress = function () { wrapper.innerHTML = xhr.responseText; wrapper.scrollTop = wrapper.scrollHeight; }; xhr.onload = function () { if (xhr.status === 200) { // 确保执行完成后调用回调 if (typeof callback === 'function') { callback(); } } else { wrapper.innerHTML += '

检测失败,请检查服务器。

'; } }; xhr.onerror = function () { wrapper.innerHTML += '

请求出错,请检查网络连接。

'; }; xhr.send(); } // 显示版本更新日志 function showVersionLog(doCheckUpdate = false) { fetch(`manage.php?get_version_log=true&do_check_update=${doCheckUpdate}`) .then(response => response.json()) .then(data => { if (data.success) { if (!doCheckUpdate || data.is_updated) { showModalWithMessage("versionLogModal", "versionLogMessage", data.content); } } else { showMessageModal(data.message || '获取版本日志失败'); } }) .catch(() => { showMessageModal('无法获取版本日志,请稍后重试'); }); } // 显示使用说明 function showHelpModal() { showModalWithMessage("helpModal"); } // 更新 EPG 内容 function updateEpgContent(epgData) { document.getElementById('epgTitle').innerHTML = epgData.channel; document.getElementById('epgSource').innerHTML = `来源:${epgData.source}`; document.getElementById('epgDate').innerHTML = epgData.date; var epgContent = document.getElementById("epgContent"); epgContent.value = epgData.epg; epgContent.scrollTop = 0; } // 更新日志表格 function updateLogTable(logData) { var logTableBody = document.querySelector("#logTable tbody"); logTableBody.innerHTML = ''; logData.forEach(log => { var row = document.createElement("tr"); row.innerHTML = ` ${new Date(log.timestamp).toLocaleString('zh-CN').replace(' ', '
')} ${log.log_message} `; logTableBody.appendChild(row); }); var logTableContainer = document.getElementById("log-table-container"); logTableContainer.scrollTop = logTableContainer.scrollHeight; } // 更新 cron 日志内容 function updateCronLogContent(logData) { var logContent = document.getElementById("cronLogContent"); logContent.value = logData.map(log => `[${new Date(log.timestamp).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })}] ${log.log_message}`) .join('\n'); logContent.scrollTop = logContent.scrollHeight; } // 显示频道别名列表 function updateChannelList(channelsData) { const channelTitle = document.getElementById('channelModalTitle'); channelTitle.innerHTML = `频道列表(总数:${channelsData.count})`; // 更新频道总数 document.getElementById('channelTable').dataset.allChannels = JSON.stringify(channelsData.channels); // 将原始频道和映射后的频道数据存储到 dataset 中 filterChannels('channel'); // 生成数据 } // 显示台标列表 function updateIconList(iconsData) { const channelTitle = document.getElementById('iconModalTitle'); channelTitle.innerHTML = `频道列表(总数:${iconsData.count})`; // 更新频道总数 document.getElementById('iconTable').dataset.allIcons = JSON.stringify(iconsData.channels); // 将频道名和台标地址存储到 dataset 中 filterChannels('icon'); // 生成数据 } // 显示频道绑定 EPG 列表 function updateChannelBindEPGList(channelBindEPGData) { // 创建并添加隐藏字段 const channelBindEPGInput = document.createElement('input'); channelBindEPGInput.type = 'hidden'; channelBindEPGInput.name = 'channel_bind_epg'; document.getElementById('settingsForm').appendChild(channelBindEPGInput); document.getElementById('channelBindEPGTable').dataset.allChannelBindEPG = JSON.stringify(channelBindEPGData); var channelBindEPGTableBody = document.querySelector("#channelBindEPGTable tbody"); var allChannelBindEPG = JSON.parse(document.getElementById('channelBindEPGTable').dataset.allChannelBindEPG); channelBindEPGInput.value = JSON.stringify(allChannelBindEPG); // 清空现有表格 channelBindEPGTableBody.innerHTML = ''; allChannelBindEPG.forEach(channelbindepg => { var row = document.createElement('tr'); row.innerHTML = ` ${String(channelbindepg.epg_src)} ${channelbindepg.channels} `; row.querySelector('td[contenteditable]').addEventListener('input', function () { channelbindepg.channels = this.textContent; document.getElementById('channelBindEPGTable').dataset.allChannelBindEPG = JSON.stringify(allChannelBindEPG); channelBindEPGInput.value = JSON.stringify(allChannelBindEPG); }); channelBindEPGTableBody.appendChild(row); }); } // 显示频道匹配结果 function updateChannelMatchList(channelMatchdata) { const channelMatchTableBody = document.querySelector("#channelMatchTable tbody"); channelMatchTableBody.innerHTML = ''; const typeOrder = { '未匹配': 1, '反向模糊': 2, '正向模糊': 3, '别名/忽略': 4, '精确匹配': 5 }; // 处理并排序匹配数据 const sortedMatches = Object.values(channelMatchdata) .flat() .sort((a, b) => typeOrder[a.type] - typeOrder[b.type]); // 创建表格行 sortedMatches.forEach(({ ori_channel, clean_channel, match, type }) => { const matchType = type === '精确匹配' ? '' : type; const row = document.createElement("tr"); row.innerHTML = ` ${ori_channel} ${clean_channel} ${match || ''} ${matchType} `; channelMatchTableBody.appendChild(row); }); document.getElementById("channel-match-table-container").style.display = 'block'; } // 显示限定频道列表 function updateGenList(genData) { const gen_list_text = document.getElementById('gen_list_text'); if (!gen_list_text.value) { gen_list_text.value = genData.join('\n'); } } // 显示指定页码的数据 function displayPage(data, page) { const tableBody = document.querySelector('#liveSourceTable tbody'); tableBody.innerHTML = ''; // 清空表格内容 const start = (page - 1) * rowsPerPage; const end = Math.min(start + rowsPerPage, data.length); if (data.length === 0) { tableBody.innerHTML = '暂无数据'; return; } // 列索引和对应字段的映射 const columns = ['groupTitle', 'channelName', 'streamUrl', 'iconUrl', 'tvgId', 'tvgName', 'resolution', 'speed', 'disable', 'modified']; // 填充当前页的表格数据 data.slice(start, end).forEach((item, index) => { const row = document.createElement('tr'); row.innerHTML = ` ${start + index + 1} ${columns.map((col, columnIndex) => { let cellContent = item[col] || ''; let cellClass = ''; // 处理 disable 和 modified 列 if (col === 'disable' || col === 'modified') { cellContent = item[col] == 1 ? '是' : '否'; cellClass = (col === 'disable' && item[col] == 1) ? 'table-cell-disable' : (col === 'modified' && item[col] == 1) ? 'table-cell-modified' : 'table-cell-clickable'; } const editable = ['resolution', 'speed', 'disable', 'modified'].includes(col) ? '' : 'contenteditable="true"'; const clickableClass = (col === 'disable' || col === 'modified') ? 'table-cell-clickable' : ''; return ` ${cellContent} `; }).join('')} `; // 为每个单元格添加事件监听器 row.querySelectorAll('td[contenteditable="true"]').forEach((cell, columnIndex) => { cell.addEventListener('input', () => { const dataIndex = (currentPage - 1) * rowsPerPage + index; if (dataIndex < allLiveData.length) { allLiveData[dataIndex][columns[columnIndex]] = cell.textContent.trim(); allLiveData[dataIndex]['modified'] = 1; // 标记修改位 const lastCell = cell.closest('tr').lastElementChild; lastCell.textContent = '是'; lastCell.classList.add('table-cell-modified'); } }); }); // 为 disable 和 modified 列添加点击事件,切换 "是/否" row.querySelectorAll('td.table-cell-clickable').forEach((cell, columnIndex) => { cell.addEventListener('click', () => { const dataIndex = (currentPage - 1) * rowsPerPage + index; if (dataIndex < allLiveData.length) { const isDisable = columnIndex === 0; const field = isDisable ? 'disable' : 'modified'; const newValue = allLiveData[dataIndex][field] == 1 ? 0 : 1; allLiveData[dataIndex][field] = newValue; cell.textContent = newValue == 1 ? '是' : '否'; if (isDisable) { cell.classList.toggle('table-cell-disable', newValue == 1); allLiveData[dataIndex]['modified'] = 1; // 标记修改位 const lastCell = cell.closest('tr').lastElementChild; lastCell.textContent = '是'; lastCell.classList.add('table-cell-modified'); } else { cell.classList.toggle('table-cell-modified', newValue == 1); } } }); }); tableBody.appendChild(row); }); } // 创建分页控件 function setupPagination(data) { const paginationContainer = document.getElementById('paginationContainer'); paginationContainer.innerHTML = ''; // 清空分页容器 const totalPages = Math.ceil(data.length / rowsPerPage); document.getElementById('live-source-table-container').style.height = totalPages <= 1 ? "410px" : "375px"; if (totalPages <= 1) return; const maxButtons = 11; // 总显示按钮数,包括“<”和“>” const pageButtons = maxButtons - 2; // 除去 "<" 和 ">" 的按钮数 // 创建按钮 const createButton = (text, page, isActive = false, isDisabled = false) => { const button = document.createElement('button'); button.textContent = text; button.className = isActive ? 'active' : ''; button.disabled = isDisabled; button.onclick = () => { if (!isDisabled) { currentPage = page; displayPage(data, currentPage); // 更新页面显示内容 setupPagination(data); // 更新分页控件 } }; return button; }; // 前部 paginationContainer.appendChild(createButton('<', currentPage - 1, false, currentPage === 1)); paginationContainer.appendChild(createButton(1, 1, currentPage === 1)); if (currentPage > 5 && totalPages > pageButtons) paginationContainer.appendChild(createButton('...', null, false, true)); // 中部 let startPage = Math.max(2, currentPage - Math.floor(pageButtons / 2) + 2); let endPage = Math.min(totalPages - 1, currentPage + Math.floor(pageButtons / 2) - 2); if (currentPage <= 5) { startPage = 2; endPage = Math.min(pageButtons - 2, totalPages - 1); } else if (currentPage >= totalPages - 4) { startPage = Math.max(totalPages - pageButtons + 3, 2); endPage = totalPages - 1; } for (let i = startPage; i <= endPage; i++) { paginationContainer.appendChild(createButton(i, i, currentPage === i)); } // 后部 if (currentPage < totalPages - 4 && totalPages > pageButtons) paginationContainer.appendChild(createButton('...', null, false, true)); paginationContainer.appendChild(createButton(totalPages, totalPages, currentPage === totalPages)); paginationContainer.appendChild(createButton('>', currentPage + 1, false, currentPage === totalPages)); } let currentPage = 1; // 当前页码 const rowsPerPage = 100; // 每页显示的行数 let allLiveData = []; // 用于存储直播源数据 // 更新模态框内容并初始化分页 function updateLiveSourceModal(data) { document.getElementById('sourceUrlTextarea').value = data.source_content || ''; document.getElementById('liveTemplateTextarea').value = data.template_content || ''; const channels = Array.isArray(data.channels) ? data.channels : []; allLiveData = channels; // 将所有数据保存在全局变量中 currentPage = 1; // 重置为第一页 displayPage(channels, currentPage); // 显示第一页数据 setupPagination(channels); // 初始化分页控件 } // 上传直播源文件 document.getElementById('liveSourceFile').addEventListener('change', function () { const file = this.files[0]; const allowedExtensions = ['m3u', 'txt']; const fileExtension = file.name.split('.').pop().toLowerCase(); // 检查文件类型 if (!allowedExtensions.includes(fileExtension)) { showMessageModal('只接受 .m3u 和 .txt 文件'); return; } // 创建 FormData 并发送 AJAX 请求 const formData = new FormData(); formData.append('liveSourceFile', file); fetch('manage.php', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { showModal('live'); } else { showMessageModal('上传失败: ' + data.message); } }) .catch(error => showMessageModal('上传过程中发生错误:' + error)); this.value = ''; // 重置文件输入框的值,确保可以连续上传相同文件 }); // 设置直播源自动同步、优化频道名开关 function toggleStatus(toggleBtn) { fetch(`manage.php?toggle_status=true&toggle_button=${toggleBtn}`) .then(response => response.json()) .then(data => { // 更新按钮显示 document.getElementById(toggleBtn).innerHTML = `${toggleBtn === "toggleLiveSourceSyncBtn" ? "同步更新" : "频道更名"}: ${data.status === 1 ? "是" : "否"}`; const syncStatus = document.getElementById("toggleLiveSourceSyncBtn").innerHTML; const processStatus = document.getElementById("toggleLiveChannelNameProcessBtn").innerHTML; document.getElementById('showMoreLiveSettingBtn').setAttribute('onclick', `showMoreLiveSetting('${syncStatus}', '${processStatus}')`); }) .catch(error => console.error("Error:", error)); } // 保存编辑后的直播源地址 function saveLiveSourceFile() { source = document.getElementById('sourceUrlTextarea'); const sourceContent = source.value.replace(/^\s*[\r\n]+/gm, '').replace(/\n$/, ''); source.value = sourceContent; // 内容写入 source.txt 文件 fetch('manage.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ save_content_to_file: 'true', file_path: '/data/live/source.txt', content: sourceContent }) }) .catch(error => { showMessageModal('保存失败: ' + error); }); } document.getElementById('sourceUrlTextarea').addEventListener('blur', saveLiveSourceFile); // 显示更多直播源设置 function showMoreLiveSetting(sourceSync, nameProcess) { showMessageModal(''); document.getElementById('messageModalMessage').innerHTML = `
`; } // 保存编辑后的直播源信息 function saveLiveSourceInfo(popup = true, filePath = '') { // 获取 checkbox 配置 const liveTvgLogoEnable = document.getElementById('live_tvg_logo_enable').value; const liveTvgIdEnable = document.getElementById('live_tvg_id_enable').value; const liveTvgNameEnable = document.getElementById('live_tvg_name_enable').value; // 保存直播源信息 fetch('manage.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ save_source_info: 'true', live_tvg_logo_enable: liveTvgLogoEnable, live_tvg_id_enable: liveTvgIdEnable, live_tvg_name_enable: liveTvgNameEnable, file_path: filePath, content: JSON.stringify(allLiveData) }) }) .then(response => response.json()) .then(data => { if (popup) { showMessageModal(data.success ? '保存成功
已生成 M3U 及 TXT 文件' : '保存失败'); } }) .catch(error => { if (popup) { showMessageModal('保存过程中出现错误: ' + error); } }); } // 直播源信息另存为新文件 function saveLiveSourceInfoAs() { showMessageModal(''); document.getElementById('messageModalMessage').innerHTML = `

另存为

`; // 添加按钮点击事件,点击后另存为新文件 document.getElementById('confirmBtn').onclick = function () { fileName = document.getElementById('fileName').value; saveLiveSourceInfo(popup = false, fileName); // 检查并添加 fileName 到文本框 let t = document.getElementById('sourceUrlTextarea'); if (!t.value.split('\n').some(line => line.replace(/[#\s]/g, '').trim() === fileName)) { t.value += `\n# ${fileName}`; t.scrollTop = t.scrollHeight; saveLiveSourceFile(); } const [token, serverUrl, tokenRange] = document.getElementById('showLiveUrlBtn') .getAttribute('onclick') .match(/\`(.*?)\`/g) .map(s => s.slice(1, -1)); var tokenStr = (tokenRange == 1 || tokenRange == 3) ? `token=${token}&` : ''; var m3uUrl = `${serverUrl}/index.php?${tokenStr}live=m3u&url=${fileName}`; var txtUrl = `${serverUrl}/index.php?${tokenStr}live=txt&url=${fileName}`; message = `成功另存为 ${fileName}
M3U:
${m3uUrl}
TXT:
${txtUrl}`; showMessageModal(message); }; } // 检验每个直播源的访问速度及分辨率 function checkSource() { showMessageModal(''); const messageContainer = document.getElementById('messageModalMessage'); // 设置说明和开始测试、清除结果按钮 messageContainer.innerHTML = `
即将开始检测每个直播源的访问速度及分辨率,
该过程可能需要一些时间,请耐心等待。

注意:结果不一定准确,且暂无法解析 IPv6 源。

`; // 开始测速 document.getElementById('confirmCheckBtn').onclick = function () { showExecResult('check.php', () => showModal('live', popup = false)); }; // 清除结果 document.getElementById('cleanCheckResultBtn').onclick = function () { showExecResult('check.php?cleanMode=true', () => showModal('live', popup = false), fullSize = false); }; } // 清理未使用的直播源文件 function cleanUnusedSource() { fetch('manage.php?delete_unused_live_data=true') .then(response => response.json()) .then(data => { if (data.success) { showMessageModal(data.message); } else { showMessageModal('清理失败'); } }) .catch(error => { showMessageModal('Error: ' + error); }); } // 显示直播源地址 function showLiveUrl(token, serverUrl, tokenRange) { var tokenStr = (tokenRange == 1 || tokenRange == 3) ? `token=${token}&` : ''; var m3uUrl = `${serverUrl}/index.php?${tokenStr}live=m3u`; var txtUrl = `${serverUrl}/index.php?${tokenStr}live=txt`; message = `M3U:
${m3uUrl}下载
TXT:
${txtUrl}   下载
转换:
${m3uUrl}&url=xxx
${txtUrl}&url=xxx`; showMessageModal(message); } // 显示直播源模板 function showLiveTemplate() { showModalWithMessage("liveTemplateModal"); } // 保存编辑后的直播源模板 function saveLiveTemplate() { // 保存配置 liveTemplateEnable = document.getElementById('live_template_enable').value; liveFuzzyMatch = document.getElementById('live_fuzzy_match').value; liveUrlComment = document.getElementById('live_url_comment').value; fetch('manage.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ update_config_field: 'true', live_template_enable: liveTemplateEnable, live_fuzzy_match: liveFuzzyMatch, live_url_comment: liveUrlComment }) }); // 内容写入 template.txt 文件 liveTemplateContent = document.getElementById('liveTemplateTextarea').value; fetch('manage.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ save_content_to_file: 'true', file_path: '/data/live/template.txt', content: liveTemplateContent }) }) .then(response => response.json()) .then(data => { if (data.success) { parseSourceInfo("保存成功
正在重新解析..."); document.getElementById('liveTemplateModal').style.display = 'none'; } else { showMessageModal('保存失败'); } }) .catch(error => { showMessageModal('保存失败: ' + error); }); } // 搜索频道 function filterChannels(type) { const tableId = type === 'channel' ? 'channelTable' : 'iconTable'; const dataAttr = type === 'channel' ? 'allChannels' : 'allIcons'; const input = document.getElementById(type === 'channel' ? 'channelSearchInput' : 'iconSearchInput').value.toUpperCase(); const tableBody = document.querySelector(`#${tableId} tbody`); const allData = JSON.parse(document.getElementById(tableId).dataset[dataAttr]); tableBody.innerHTML = ''; // 清空表格 // 创建行的通用函数 function createEditableRow(item, itemIndex, insertAfterRow = null) { const row = document.createElement('tr'); row.innerHTML = ` 创建自定义频道${item.channel || ''} ${item.icon || ''} `; // 动态更新 allData row.querySelectorAll('td[contenteditable]').forEach(cell => { cell.addEventListener('input', () => { allData[itemIndex][cell.getAttribute('name')] = cell.textContent.trim(); document.getElementById(tableId).dataset[dataAttr] = JSON.stringify(allData); if (cell.getAttribute('name') === 'channel' && item.channel && !allData.some(e => !e.channel)) { allData.push({ channel: '', icon: '' }); createEditableRow(allData[allData.length - 1], allData.length - 1, row); // 插入新行到当前行后 } }); }); // 上传文件 row.querySelector(`#icon_new_${itemIndex}`).addEventListener('change', event => handleIconFileUpload(event, item, row, allData)); // 如果指定了插入位置,则插入到该行之后,否则追加到表格末尾 if (insertAfterRow) { insertAfterRow.insertAdjacentElement('afterend', row); } else { tableBody.appendChild(row); } } // 创建初始空行(仅用于 icon) if (!input && type === 'icon') { allData.push({ channel: '', icon: '' }); createEditableRow(allData[allData.length - 1], allData.length - 1); } // 筛选并显示行的逻辑 allData.forEach((item, index) => { const searchText = type === 'channel' ? item.original : item.channel; if (String(searchText).toUpperCase().includes(input)) { const row = document.createElement('tr'); if (type === 'channel') { row.innerHTML = ` ${item.original} ${item.mapped || ''}`; row.querySelector('td[contenteditable]').addEventListener('input', function () { item.mapped = this.textContent.trim(); document.getElementById(tableId).dataset[dataAttr] = JSON.stringify(allData); }); } else if (type === 'icon' && searchText) { row.innerHTML = ` ${item.channel} ${item.icon || ''} ${item.icon ? `` : ''} `; row.querySelectorAll('td[contenteditable]').forEach((cell, idx) => { cell.addEventListener('input', function () { if (idx === 0) item.channel = this.textContent.trim(); // 第一个可编辑单元格更新 channel else item.icon = this.textContent.trim(); // 第二个可编辑单元格更新 icon document.getElementById(tableId).dataset[dataAttr] = JSON.stringify(allData); }); }); row.querySelector(`#file_${index}`).addEventListener('change', event => handleIconFileUpload(event, item, row, allData)); } tableBody.appendChild(row); } }); } // 台标上传 function handleIconFileUpload(event, item, row, allData) { const file = event.target.files[0]; if (file && file.type === 'image/png') { const formData = new FormData(); formData.append('iconFile', file); fetch('manage.php', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { const iconUrl = data.iconUrl; row.cells[1].innerText = iconUrl; item.icon = iconUrl; row.cells[2].innerHTML = ` `; document.getElementById('iconTable').dataset.allIcons = JSON.stringify(allData); updateIconListJsonFile(); } else { showMessageModal('上传失败:' + data.message); } }) .catch(error => showMessageModal('上传过程中发生错误:' + error)); } else { showMessageModal('请选择PNG文件上传'); } event.target.value = ''; // 重置文件输入框的值,确保可以连续上传相同文件 } // 转存所有台标到服务器 function uploadAllIcons() { const serverUrl = window.location.origin; const iconTable = document.getElementById('iconTable'); const allIcons = JSON.parse(iconTable.dataset.allIcons); const rows = Array.from(document.querySelectorAll('#iconTable tbody tr')); let totalIcons = 0; let uploadedIcons = 0; const rowsToUpload = rows.filter(row => { const iconUrl = row.cells[1]?.innerText.trim(); if (iconUrl) { totalIcons++; if (!iconUrl.startsWith(serverUrl)) { return true; } else { uploadedIcons++; } } return false; }); const progressDisplay = document.getElementById('progressDisplay') || document.createElement('div'); progressDisplay.id = 'progressDisplay'; progressDisplay.style.cssText = 'margin: 10px 0; text-align: right;'; progressDisplay.textContent = `已转存 ${uploadedIcons}/${totalIcons}`; iconTable.before(progressDisplay); const uploadPromises = rowsToUpload.map(row => { const [channelCell, iconCell, previewCell] = row.cells; const iconUrl = iconCell?.innerText.trim(); const fileName = decodeURIComponent(iconUrl.split('/').pop().split('?')[0]); return fetch(iconUrl) .then(res => res.blob()) .then(blob => { const formData = new FormData(); formData.append('iconFile', new File([blob], fileName, { type: 'image/png' })); return fetch('manage.php', { method: 'POST', body: formData }); }) .then(res => res.json()) .then(data => { if (data.success) { const iconUrl = data.iconUrl; const channelName = channelCell.innerText.trim(); iconCell.innerText = iconUrl; previewCell.innerHTML = ` `; allIcons.forEach(item => { if (item.channel === channelName) item.icon = iconUrl; }); iconTable.dataset.allIcons = JSON.stringify(allIcons); uploadedIcons++; progressDisplay.textContent = `已转存 ${uploadedIcons}/${totalIcons}`; } else { previewCell.innerHTML = `上传失败: ${data.message}`; } }) .catch(() => { previewCell.innerHTML = '上传出错'; }); }); Promise.all(uploadPromises).then(() => { if (uploadedIcons !== totalIcons) { uploadAllIcons(); // 继续上传 } else { updateIconListJsonFile(); showMessageModal("全部转存成功,已保存!"); } }); } // 清理未使用的台标文件 function deleteUnusedIcons() { fetch('manage.php?delete_unused_icons=true') .then(response => response.json()) .then(data => { if (data.success) { showMessageModal(data.message); } else { showMessageModal('清理失败'); } }) .catch(error => { showMessageModal('Error: ' + error); }); } // 更新频道别名 function updateChannelMapping() { var allChannels = JSON.parse(document.getElementById('channelTable').dataset.allChannels); var existingMappings = document.getElementById('channel_mappings').value.split('\n'); // 过滤出现有映射中的正则表达式映射 var regexMappings = existingMappings.filter(line => line.includes('regex:')); // 生成新的频道别名映射 var newMappings = allChannels .filter(channel => channel.mapped.trim() !== '') .map(channel => `${channel.original} => ${channel.mapped}`); // 更新映射文本框并保存配置 document.getElementById('channel_mappings').value = [...newMappings, ...regexMappings].join('\n'); updateConfig(); } // 解析 txt、m3u 直播源,并生成频道列表(仅频道) async function parseSource() { const textarea = document.getElementById('gen_list_text'); let text = textarea.value.trim(); const channels = new Set(); // 拆分输入的内容,可能包含多个 URL 或文本 if (!text.includes('#EXTM3U')) { let lines = text.split('\n').map(line => line.trim()); let urls = lines.filter(line => line.startsWith('http')); // 如果存在 URL,则清空原本的 text 内容并逐个请求获取数据 if (urls.length > 0) { text = ''; for (let url of urls) { try { const response = await fetch('manage.php?download_data=true&url=' + encodeURIComponent(url)); const result = await response.json(); // 解析 JSON 响应 if (result.success && !/not found/i.test(result.data)) { text += '\n' + result.data; } else { showMessageModal(/not found/i.test(result.data) ? `Error: ${result.data}` : `${result.message}:\n${url}`); } } catch (error) { showMessageModal(`无法获取URL内容: ${url}\n错误信息: ${error.message}`); // 显示网络错误信息 } } } } // 处理 m3u 、 txt 文件内容 text.split('\n').forEach(line => { if (line && !/^http/i.test(line) && !/#genre#/i.test(line) && !/#extm3u/i.test(line)) { if (/^#extinf:/i.test(line)) { const tvgIdMatch = line.match(/tvg-id="([^"]+)"/i); const tvgNameMatch = line.match(/tvg-name="([^"]+)"/i); channelName = (tvgIdMatch && /\D/.test(tvgIdMatch[1]) ? tvgIdMatch[1] : tvgNameMatch ? tvgNameMatch[1] : line.split(',').slice(-1)[0]).trim(); } else { channelName = line.split(',')[0].trim(); } if (channelName) channels.add(channelName.toUpperCase()); } }); // 将解析后的频道列表放回文本区域 textarea.value = Array.from(channels).join('\n'); // 保存限定频道列表到数据库 setGenList(); } // 解析 txt、m3u 直播源,并生成直播列表(包含分组、地址等信息) function parseSourceInfo(message = '') { showMessageModal(message || "在线源解析较慢
请耐心等待..."); fetch(`manage.php?parse_source_info=true`) .then(response => response.json()) .then(data => { showModal('live'); if (data.success == 'full') { showMessageModal('解析成功
已生成 M3U 及 TXT 文件'); } else if (data.success == 'part') { showMessageModal('已生成 M3U 及 TXT 文件
部分源异常
' + data.message); } }) .catch(error => showMessageModal('解析过程中发生错误:' + error)); } // 保存限定频道列表 async function setGenList() { const genListText = document.getElementById('gen_list_text').value; try { const response = await fetch('manage.php?set_gen_list=true', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ data: genListText }) }); const responseText = await response.text(); if (responseText.trim() !== 'success') { console.error('服务器响应错误:', responseText); } } catch (error) { console.error(error); } } // 保存限定频道列表并更新配置 function setGenListAndUpdateConfig() { setGenList(); updateConfig(); } // 更新台标文件 iconList.json function updateIconListJsonFile(notify = false) { var iconTableElement = document.getElementById('iconTable'); var allIcons = iconTableElement && iconTableElement.dataset.allIcons ? JSON.parse(iconTableElement.dataset.allIcons) : null; if (allIcons) { fetch('manage.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ update_icon_list: true, updatedIcons: JSON.stringify(allIcons) // 传递更新后的图标数据 }) }) .then(response => response.json()) .then(data => { if (data.success && notify) { showModal('icon'); showMessageModal('保存成功'); } else if (data.success == false) { showMessageModal(data.message); } }) .catch(error => showMessageModal('更新过程中发生错误:' + error)); } } // 导入配置 document.getElementById('importFile').addEventListener('change', function () { const file = this.files[0]; const fileExtension = file.name.split('.').pop().toLowerCase(); // 检查文件类型 if (fileExtension != 'gz') { showMessageModal('只接受 .gz 文件'); return; } // 发送 AJAX 请求 const formData = new FormData(document.getElementById('importForm')); fetch('manage.php', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { showMessageModal(data.message); if (data.success) { // 延迟刷新页面 setTimeout(() => { window.location.href = 'manage.php'; }, 3000); } }) .catch(error => showMessageModal('导入过程中发生错误:' + error)); this.value = ''; // 重置文件输入框的值,确保可以连续上传相同文件 }); // 修改 token、user_agent 对话框 function changeTokenUA(type, currentTokenUA) { showMessageModal(''); typeStr = (type === 'token' ? 'Token' : 'User-Agent') + '
支持多个,逗号分隔'; document.getElementById('messageModalMessage').innerHTML = `

修改 ${typeStr}

`; } // 更新 token、user_agent 到 config.json function updateTokenUA(type) { var newTokenUA = document.getElementById('newTokenUA').value.replace(/,/g, ","); // 将中文逗号替换为英文逗号 // 内容写入 config.json 文件 fetch('manage.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ update_config_field: 'true', [type.toLowerCase()]: newTokenUA }) }) .then(response => response.json()) .then(data => { if (data.success) { if (type.toLowerCase() == 'token' || newTokenUA == '') { alert('修改成功'); window.location.href = 'manage.php'; } else { showMessageModal('修改成功'); document.getElementById('change_ua_span').setAttribute('onclick', `changeTokenUA('user_agent', '${newTokenUA}')`); } } else { showMessageModal('修改失败'); } }) .catch(error => showMessageModal('保存过程中出现错误: ' + error)); } // token_range 更变后进行提示 function showTokenRangeMessage(token, serverUrl) { var tokenRange = document.getElementById("token_range").value; var message = ''; var baseUrl = serverUrl + '/index.php?token=' + token; if (tokenRange == "1" || tokenRange == "3") { message += `直播源地址:
${baseUrl}&live=m3u
${baseUrl}&live=txt`; } if (tokenRange == "2" || tokenRange == "3") { if (message) message += '
'; message += `EPG地址:
${baseUrl}`; } if (message) { showMessageModal(message); } document.getElementById('showLiveUrlBtn').setAttribute('onclick', `showLiveUrl('${token}', '${serverUrl}', '${tokenRange}')`); } // 切换主题 document.getElementById('themeSwitcher').addEventListener('click', function () { // 获取当前主题,并切换到下一个主题 const currentTheme = localStorage.getItem('theme'); const newTheme = currentTheme === 'light' ? 'dark' : (currentTheme === 'dark' ? '' : 'light'); // 更新主题 document.body.classList.add('theme-transition'); document.body.classList.remove('dark', 'light'); if (newTheme === '') { const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)").matches; document.body.classList.add(prefersDarkScheme ? 'dark' : 'light'); } else { document.body.classList.add(newTheme); } // 更新图标和文字 document.getElementById("themeIcon"); const labelText = document.querySelector('.label-text'); themeIcon.className = `fas ${newTheme === 'dark' ? 'fa-moon' : newTheme === 'light' ? 'fa-sun' : 'fa-adjust'}`; labelText.textContent = newTheme === 'dark' ? 'Dark' : newTheme === 'light' ? 'Light' : 'Auto'; // 保存到本地存储 localStorage.setItem('theme', newTheme); }); // 监听系统主题变化 window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => { if (!localStorage.getItem('theme')) { const theme = e.matches ? 'dark' : 'light'; document.body.classList.remove('dark', 'light'); document.body.classList.add(theme); } });