';
echo '
更新数据';
// 引入公共脚本
require_once 'public.php';
ini_set('memory_limit', '256M');
// 设置超时时间为20分钟
set_time_limit(20*60);
// 删除过期数据和日志
function deleteOldData($db, &$log_messages) {
global $Config, $thresholdDate;
// 删除 t.xml 和 t.xml.gz 文件
@unlink(__DIR__ . '/t.xml');
@unlink(__DIR__ . '/t.xml.gz');
logMessage($log_messages, "[Start] 开始生成数据...");
echo "请注意:在生成数据期间无法访问XMLTV文件和API接口";
echo "
";
// 循环清理过期数据
$tables = [
'epg_data' => ['date', '清理EPG数据'],
'update_log' => ['timestamp', '清理更新日志'],
'cron_log' => ['timestamp', '清理定时日志']
];
foreach ($tables as $table => $values) {
list($column, $logMessage) = $values;
$stmt = $db->prepare("DELETE FROM $table WHERE $column < :thresholdDate");
$stmt->bindValue(':thresholdDate', $thresholdDate, PDO::PARAM_STR);
$stmt->execute();
logMessage($log_messages, "[{$logMessage}] 共 {$stmt->rowCount()} 条。");
}
// 清理 memcached 数据
if (class_exists('Memcached')) {
$memcached = new Memcached();
if ($memcached->addServer('127.0.0.1', 11211)) {
$memcached->flush();
logMessage($log_messages, "[Memcached] 已清空。");
} else {
logMessage($log_messages, "[Memcached] 状态异常。");
}
} else {
logMessage($log_messages, "[Memcached] 未安装。");
}
// 清理 redis 数据
if (class_exists('Redis')) {
$redis = new Redis();
try {
$redis->connect('127.0.0.1', 6379);
if (!empty($Config['redis_password'])) {
$redis->auth($Config['redis_password']);
}
$redis->flushAll();
logMessage($log_messages, "[Redis] 已清空。");
} catch (Exception $e) {
logMessage($log_messages, "[Redis] 状态异常:" . $e->getMessage());
}
} else {
logMessage($log_messages, "[Redis] 未安装。");
}
echo "
";
}
// 格式化时间函数,同时转化为 UTC+8 时间
function getFormatTime($time, $overwrite_time_zone) {
if (empty($time)) return ['', ''];
$time = $overwrite_time_zone ? substr($time, 0, -5) . $overwrite_time_zone : $time;
$time = str_replace(' ', '', $time);
$datetime = DateTime::createFromFormat('YmdHisO', $time);
if (!$datetime) return [null, null];
$datetime->setTimezone(new DateTimeZone('+0800'));
return [$datetime->format('Y-m-d'), $datetime->format('H:i')];
}
// 辅助函数:将日期和时间格式化为 XMLTV 格式
function formatTime($date, $time) {
return date('YmdHis O', strtotime("$date $time"));
}
// 获取限定频道列表及映射关系
function getGenList($db) {
global $Config;
$channels = $db->query("SELECT channel FROM gen_list")->fetchAll(PDO::FETCH_COLUMN);
if (empty($channels)) {
return ['gen_list_mapping' => [], 'gen_list' => []];
}
$channelsSimplified = explode("\n", t2s(implode("\n", $channels)));
$allEpgChannels = $db->query("SELECT DISTINCT channel FROM epg_data WHERE date = DATE('now')")
->fetchAll(PDO::FETCH_COLUMN); // 避免匹配只有历史 EPG 的频道
$gen_list_mapping = [];
$cleanedChannels = array_map('cleanChannelName', $channelsSimplified);
foreach ($cleanedChannels as $index => $cleanedChannel) {
$bestMatch = $cleanedChannel; // 默认使用清理后的频道名
$bestMatchLength = 0; // 初始为0,表示未找到任何匹配
foreach ($allEpgChannels as $epgChannel) {
if (strcasecmp($cleanedChannel, $epgChannel) === 0) {
$bestMatch = $epgChannel;
break; // 精确匹配,立即跳出循环
}
// 模糊匹配并选择最长的频道名称
if ((stripos($epgChannel, $cleanedChannel) === 0 || stripos($cleanedChannel, $epgChannel) !== false)
&& strlen($epgChannel) > $bestMatchLength) {
$bestMatch = $epgChannel;
$bestMatchLength = strlen($epgChannel); // 更新为更长的匹配
}
}
// 将原始频道名称添加到映射数组中
$gen_list_mapping[$bestMatch][] = $channels[$index];
}
return [
'gen_list_mapping' => $gen_list_mapping,
'gen_list' => array_unique($cleanedChannels)
];
}
// 获取频道指定 EPG 关系
function getChannelBindEPG() {
global $Config;
$channelBindEPG = [];
foreach ($Config['channel_bind_epg'] ?? [] as $epg_src => $channels) {
foreach (array_map('trim', explode(',', $channels)) as $channel) {
$channelBindEPG[$channel][] = $epg_src;
}
}
return $channelBindEPG;
}
// 下载 XML 数据并存入数据库
function downloadXmlData($xml_url, $userAgent, $db, &$log_messages, $gen_list) {
global $Config;
$xml_data = downloadData($xml_url, $userAgent);
if ($xml_data !== false && stripos($xml_data, 'not found') === false) {
if (substr($xml_data, 0, 2) === "\x1F\x8B") { // 通过魔数判断 .gz 文件
$xml_data = gzdecode($xml_data);
if ($xml_data === false) {
logMessage($log_messages, ' [解压缩失败!!!]');
return;
}
}
// 获取文件大小(字节)并转换为 KB/MB
$fileSize = strlen($xml_data);
$fileSizeReadable = $fileSize >= 1048576
? round($fileSize / 1048576, 2) . ' MB'
: round($fileSize / 1024, 2) . ' KB';
logMessage($log_messages, "[下载] 成功: xml 文件 {$fileSizeReadable}");
$xml_data = preg_replace('/[\x00-\x1F]/u', ' ', $xml_data); // 清除所有控制字符
if (isset($Config['all_chs']) && $Config['all_chs']) { $xml_data = t2s($xml_data); }
$db->beginTransaction();
try {
$processCount = processXmlData($xml_url, $xml_data, $db, $gen_list);
$db->commit();
logMessage($log_messages, "[更新] 成功:共 {$processCount} 条");
} catch (Exception $e) {
$db->rollBack();
logMessage($log_messages, "[处理数据出错], 错误原因: " . $e->getMessage());
}
} else {
logMessage($log_messages, "[下载EPG数据] 失败!!!");
}
echo "
";
}
// 处理 XML 数据并逐步存入数据库
function processXmlData($xml_url, $xml_data, $db, $gen_list) {
global $Config, $processedRecords, $channel_bind_epg, $thresholdDate;
// 统计处理数据量
$processCount = 0;
$reader = new XMLReader();
if (!$reader->XML($xml_data)) {
throw new Exception("无法解析 XML 数据");
}
$cleanChannelNames = [];
// 读取频道数据
while ($reader->read() && $reader->name !== 'channel');
while ($reader->name === 'channel') {
$channel = new SimpleXMLElement($reader->readOuterXML());
$channelId = (string)$channel['id'];
$cleanChannelNames[$channelId] = cleanChannelName((string)$channel->{'display-name'});
$reader->next('channel');
}
// 繁简转换和频道筛选
$simplifiedChannelNames = (isset($Config['all_chs']) && $Config['all_chs']) ?
$cleanChannelNames : explode("\n", t2s(implode("\n", $cleanChannelNames)));
$channelNamesMap = [];
foreach ($cleanChannelNames as $channelId => $channelName) {
$channelNameSimplified = array_shift($simplifiedChannelNames);
// 假如 channel_bind_epg 存在且频道在其中有记录,且不为当前 xml_url,直接跳过
if (!empty($channel_bind_epg) &&
isset($channel_bind_epg[$channelNameSimplified]) &&
!in_array($xml_url, $channel_bind_epg[$channelNameSimplified])
) {
continue; // 跳过当前循环,继续处理下一个
}
// 当 gen_list_enable 为 0 时,插入所有数据
if (empty($Config['gen_list_enable'])) {
$channelNamesMap[$channelId] = $channelNameSimplified;
continue;
}
$matchFound = false;
foreach ($gen_list as $item) {
if (stripos($channelNameSimplified, $item) !== false ||
stripos($item, $channelNameSimplified) !== false) {
$matchFound = true;
break;
}
}
if ($matchFound) {
$channelNamesMap[$channelId] = $channelNameSimplified;
}
}
$reader->close();
$reader->XML($xml_data); // 重置 XMLReader
while ($reader->read() && $reader->name !== 'programme');
$currentChannelProgrammes = [];
$crossDayProgrammes = []; // 保存跨天的节目数据
// 修正 epg.pw 时区错误
$overwrite_time_zone = strpos($xml_data, 'epg.pw') !== false ? '+0800' : '';
while ($reader->name === 'programme') {
$programme = new SimpleXMLElement($reader->readOuterXML());
[$startDate, $startTime] = getFormatTime((string)$programme['start'], $overwrite_time_zone);
[$endDate, $endTime] = getFormatTime((string)$programme['stop'], $overwrite_time_zone);
// 判断数据是否符合设定期限
if (empty($startDate) || $startDate < $thresholdDate || empty($endDate)) {
$reader->next('programme');
continue;
}
$channelId = (string)$programme['channel'];
$channelName = $channelNamesMap[$channelId] ?? null;
$recordKey = $channelName . '-' . $startDate;
// 优先处理跨天数据
if (isset($crossDayProgrammes[$channelId][$startDate]) && !isset($processedRecords[$recordKey])) {
$currentChannelProgrammes[$channelId]['diyp_data'][$startDate] = array_merge(
$currentChannelProgrammes[$channelId]['diyp_data'][$startDate] ?? [],
$crossDayProgrammes[$channelId][$startDate]
);
$currentChannelProgrammes[$channelId]['channel_name'] = $channelName;
unset($crossDayProgrammes[$channelId][$startDate]);
}
if ($channelName && !isset($processedRecords[$recordKey])) {
$programmeData = [
'start' => $startTime,
'end' => $startDate === $endDate ? $endTime : '00:00',
'title' => (string)$programme->title,
'desc' => isset($programme->desc) ? (string)$programme->desc : ''
];
$currentChannelProgrammes[$channelId]['diyp_data'][$startDate][] = $programmeData;
// 保存跨天的节目数据
if ($startDate !== $endDate && $endTime !== '00:00') {
$crossDayProgrammes[$channelId][$endDate][] = [
'start' => '00:00',
'end' => $endTime,
'title' => $programmeData['title'],
'desc' => $programmeData['desc']
];
}
$currentChannelProgrammes[$channelId]['channel_name'] = $channelName;
// 每次达到 50 时,插入数据并保留最后一条
if (count($currentChannelProgrammes) >= 50) {
$lastProgramme = array_pop($currentChannelProgrammes); // 取出最后一条
insertDataToDatabase($currentChannelProgrammes, $db, $xml_url); // 插入前 49 条
$currentChannelProgrammes = [$channelId => $lastProgramme]; // 清空并重新赋值最后一条
}
}
$processCount++;
$reader->next('programme');
}
// 插入剩余的数据
if ($currentChannelProgrammes) {
insertDataToDatabase($currentChannelProgrammes, $db, $xml_url);
}
$reader->close();
return $processCount;
}
// 从 epg_data 读取数据,生成 iconList.json 及 xmltv 文件
function processIconListAndXmltv($db, $gen_list_mapping, &$log_messages) {
global $Config, $iconList, $iconListPath;
$currentDate = date('Y-m-d'); // 获取当前日期
$dateCondition = $Config['include_future_only'] ? "WHERE date >= '$currentDate'" : '';
// 合并查询
$query = "SELECT date, channel, epg_diyp FROM epg_data $dateCondition ORDER BY channel ASC, date ASC";
$stmt = $db->query($query);
// 存储节目数据以按频道分组
$channelData = [];
while ($program = $stmt->fetch(PDO::FETCH_ASSOC)) {
$channelName = $program['channel'];
$iconUrl = iconUrlMatch($channelName, $getDefault = false);
if ($iconUrl) {
$iconList[strtoupper($channelName)] = $iconUrl;
$program['icon'] = $iconUrl;
}
// gen_list_enable 为 0 或存在映射,则处理频道数据
if (empty($Config['gen_list_enable']) || isset($gen_list_mapping[$channelName])) {
$channelData[$channelName][] = $program;
}
}
// 更新 iconList.json 文件中的数据
if (file_put_contents($iconListPath,
json_encode($iconList, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)) === false) {
logMessage($log_messages, "[台标列表] 更新 iconList.json 时发生错误!!!");
} else {
logMessage($log_messages, "[台标列表] 已更新 iconList.json");
}
logMessage($log_messages, "[XMLTV] 开始生成并写入XMLTV文件...");
// 判断是否生成 xmltv 文件
if (empty($Config['gen_xml'])) {
return;
}
// 创建 XMLWriter 实例
$xmlFilePath = __DIR__ . '/t.xml';
$xmlWriter = new XMLWriter();
$xmlWriter->openUri($xmlFilePath);
$xmlWriter->startDocument('1.0', 'UTF-8');
$xmlWriter->startElement('tv');
$xmlWriter->writeAttribute('generator-info-name', 'CrestekkEPG');
$xmlWriter->writeAttribute('generator-info-url', 'https://github.com/mxdabc/epgphp');
$xmlWriter->setIndent(true);
$xmlWriter->setIndentString(' '); // 设置缩进
// 将 $Config['channel_mappings'] 中的映射值转换为数组
$channelMappings = array_map(function($mapped) {
return strpos($mapped, 'regex:') === 0 ? [$mapped] : array_map('trim', explode(',', $mapped));
}, $Config['channel_mappings']);
// 逐个频道处理
foreach ($channelData as $channelName => $programs) {
// 写入频道信息
$xmlWriter->startElement('channel');
$xmlWriter->writeAttribute('id', htmlspecialchars($channelName, ENT_XML1, 'UTF-8'));
// 为该频道生成多个 display-name ,包括原频道名、限定频道列表、频道别名
$displayNames = array_unique(array_merge(
[$channelName],
$gen_list_mapping[$channelName] ?? [],
$channelMappings[$channelName] ?? []
));
foreach ($displayNames as $displayName) {
$xmlWriter->startElement('display-name');
$xmlWriter->writeAttribute('lang', 'zh');
$xmlWriter->text(htmlspecialchars($displayName, ENT_XML1, 'UTF-8'));
$xmlWriter->endElement(); // display-name
}
$iconUrl = $programs[0]['icon'] ?? '';
if ($iconUrl) {
$xmlWriter->startElement('icon');
$xmlWriter->writeAttribute('src', $iconUrl);
$xmlWriter->endElement(); // icon
}
$xmlWriter->endElement(); // channel
// 写入该频道的所有节目数据
foreach ($programs as $programIndex => &$program) {
$data = json_decode($program['epg_diyp'], true);
$dataCount = count($data['epg_data']);
$end_date = $program['date'];
for ($index = 0; $index < $dataCount; $index++) {
$item = $data['epg_data'][$index];
$end_time = $item['end'];
// 如果结束时间为 00:00,切换到第二天的日期
if ($end_time == '00:00') {
$end_date = date('Ymd', strtotime($end_date . ' +1 day')); // 切换日期
// 合并下一个节目
if (isset($programs[$programIndex + 1])) {
$nextData = json_decode($programs[$programIndex + 1]['epg_diyp'], true);
$nextItem = $nextData['epg_data'][0] ?? null;
if ($nextItem && $nextItem['title'] == $item['title']) {
$end_time = $nextItem['end'];
array_splice($nextData['epg_data'], 0, 1); // 删除下一个节目的第一个项目
$programs[$programIndex + 1]['epg_diyp'] = json_encode($nextData);
}
}
}
// 写入当前节目
$xmlWriter->startElement('programme');
$xmlWriter->writeAttribute('channel', htmlspecialchars($channelName, ENT_XML1, 'UTF-8'));
$xmlWriter->writeAttribute('start', formatTime($program['date'], $item['start']));
$xmlWriter->writeAttribute('stop', formatTime($end_date, $end_time));
$xmlWriter->startElement('title');
$xmlWriter->writeAttribute('lang', 'zh');
$xmlWriter->text(htmlspecialchars($item['title'], ENT_XML1, 'UTF-8'));
$xmlWriter->endElement(); // title
if (!empty($item['desc'])) {
$xmlWriter->startElement('desc');
$xmlWriter->writeAttribute('lang', 'zh');
$xmlWriter->text(htmlspecialchars($item['desc'], ENT_XML1, 'UTF-8'));
$xmlWriter->endElement(); // desc
}
$xmlWriter->endElement(); // programme
}
}
}
// 结束 XML 文档
$xmlWriter->endElement(); // tv
$xmlWriter->endDocument();
$xmlWriter->flush();
logMessage($log_messages, "[XMLTV] 开始生成GZ压缩文件...");
// 所有频道数据写入完成后,生成 t.xml.gz 文件
compressXmlFile($xmlFilePath);
logMessage($log_messages, "[XMLTV] GZ压缩文件压缩成功");
logMessage($log_messages, "[XMLTV] 已生成 t.xml 和 t.xml.gz");
}
// 生成 t.xml.gz 压缩文件
function compressXmlFile($xmlFilePath) {
$gzFilePath = $xmlFilePath . '.gz';
// 打开原文件和压缩文件
$file = fopen($xmlFilePath, 'rb');
$gzFile = gzopen($gzFilePath, 'wb9'); // 最高压缩等级
// 将文件内容写入到压缩文件中
while (!feof($file)) {
gzwrite($gzFile, fread($file, 1024 * 512));
}
// 关闭文件
fclose($file);
gzclose($gzFile);
}
// 记录开始时间
$startTime = microtime(true);
// 统计更新前数据条数
$initialCount = $db->query("SELECT COUNT(*) FROM epg_data")->fetchColumn();
// 删除过期数据
$thresholdDate = date('Y-m-d', strtotime("-{$Config['days_to_keep']} days +1 day"));
deleteOldData($db, $log_messages);
// 获取限定频道列表及映射关系
$gen_res = getGenList($db);
$gen_list = $gen_res['gen_list'];
$gen_list_mapping = $gen_res['gen_list_mapping'];
// 获取频道指定 EPG 关系
$channel_bind_epg = getChannelBindEPG();
// 全局变量,用于记录已处理的记录
$processedRecords = [];
// 更新数据
foreach ($Config['xml_urls'] as $xml_url) {
// 去掉空白字符,忽略空行和以 # 开头的 URL
$xml_url = trim($xml_url);
if (empty($xml_url) || strpos($xml_url, '#') === 0) {
continue;
} elseif (preg_match('/^(tvmao|cntv)/i', $xml_url, $matches)) {
$data_source = strtolower($matches[0]);
downloadJSONData($data_source, $xml_url, $db, $log_messages);
continue;
}
// 更新 XML 数据
list($xml_url_str, , $userAgent) = explode('#', $xml_url) + [1 => '', 2 => ''];
$userAgent = trim($userAgent);
$cleaned_url = trim(strpos($xml_url_str, '=>') !== false ? explode('=>', $xml_url_str)[1] : $xml_url_str);
logMessage($log_messages, "[地址] $cleaned_url");
// 判断是否有限定频道列表并下载数据
if (strpos($xml_url_str, '=>') !== false) {
$tmp_gen_list = array_map('trim', explode(",", explode('=>', $xml_url_str)[0]));
logMessage($log_messages, "[临时] 限定频道:" . implode(", ", $tmp_gen_list));
downloadXmlData($cleaned_url, $userAgent, $db, $log_messages, $tmp_gen_list, 1);
} else {
downloadXmlData($cleaned_url, $userAgent, $db, $log_messages, $gen_list);
}
}
// 更新 iconList.json 及生成 xmltv 文件
processIconListAndXmltv($db, $gen_list_mapping, $log_messages);
// 判断是否同步更新直播源
if (isset($Config['live_source_auto_sync']) && $Config['live_source_auto_sync'] == 1) {
$parseResult = doParseSourceInfo();
if ($parseResult !== true) {
logMessage($log_messages, "[直播文件] 部分更新异常:" . rtrim(str_replace('
', '、', $parseResult), '、'));
} else {
logMessage($log_messages, "[直播文件] 已同步更新");
}
}
// 统计更新后数据条数
$finalCount = $db->query("SELECT COUNT(*) FROM epg_data")->fetchColumn();
$dif = $finalCount - $initialCount;
$msg = $dif != 0 ? ($dif > 0 ? " 增加 $dif 。" : " 减少 " . abs($dif) . " 。") : "";
// 记录结束时间
$endTime = microtime(true);
// 计算运行时间(以秒为单位)
$executionTime = round($endTime - $startTime, 1);
echo "
";
logMessage($log_messages, "[更新完成] {$executionTime} 秒。节目天数:更新前 {$initialCount} ,更新后 {$finalCount} 。" . $msg);
// 将日志信息写入数据库
$log_message_str = implode("
", $log_messages);
$timestamp = date('Y-m-d H:i:s'); // 使用设定的时区时间
$stmt = $db->prepare('INSERT INTO update_log (timestamp, log_message) VALUES (:timestamp, :log_message)');
$stmt->bindValue(':timestamp', $timestamp, PDO::PARAM_STR);
$stmt->bindValue(':log_message', $log_message_str, PDO::PARAM_STR);
$stmt->execute();
?>