diff --git a/api/save_avatar.php b/api/save_avatar.php index 4942e6d..ad86c6c 100644 --- a/api/save_avatar.php +++ b/api/save_avatar.php @@ -57,5 +57,23 @@ try { respond(false, '保存失败: ' . $e->getMessage()); } -respond(true, '保存成功', str_replace($_SERVER['DOCUMENT_ROOT'], '', $targetPath)); +$bytes = @filesize($targetPath) ?: 0; +try { + $statsRoot = __DIR__ . '/../stats'; + if (!is_dir($statsRoot)) { @mkdir($statsRoot, 0777, true); } + $dailyPath = $statsRoot . '/daily.json'; + $today = (new DateTime('now'))->format('Y-m-d'); + $daily = []; + if (file_exists($dailyPath)) { + $rawDaily = @file_get_contents($dailyPath); + $decoded = json_decode($rawDaily, true); + if (is_array($decoded)) { $daily = $decoded; } + } + if (!isset($daily[$today]) || !is_array($daily[$today])) { + $daily[$today] = ['visits' => 0, 'video_plays' => 0, 'picture_shows' => 0, 'uv' => 0, 'bandwidth_in' => 0, 'bandwidth_out' => 0]; + } + $daily[$today]['bandwidth_out'] += (int)$bytes; + @file_put_contents($dailyPath, json_encode($daily, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX); +} catch (Throwable $e) {} +respond(true, '保存成功', str_replace($_SERVER['DOCUMENT_ROOT'], '', $targetPath)); diff --git a/api/save_picture.php b/api/save_picture.php index 30b4fbe..2607f74 100644 --- a/api/save_picture.php +++ b/api/save_picture.php @@ -62,5 +62,23 @@ try { respond(false, '保存失败: ' . $e->getMessage()); } -respond(true, '保存成功', str_replace($_SERVER['DOCUMENT_ROOT'], '', $targetPath)); +$bytes = @filesize($targetPath) ?: 0; +try { + $statsRoot = __DIR__ . '/../stats'; + if (!is_dir($statsRoot)) { @mkdir($statsRoot, 0777, true); } + $dailyPath = $statsRoot . '/daily.json'; + $today = (new DateTime('now'))->format('Y-m-d'); + $daily = []; + if (file_exists($dailyPath)) { + $rawDaily = @file_get_contents($dailyPath); + $decoded = json_decode($rawDaily, true); + if (is_array($decoded)) { $daily = $decoded; } + } + if (!isset($daily[$today]) || !is_array($daily[$today])) { + $daily[$today] = ['visits' => 0, 'video_plays' => 0, 'picture_shows' => 0, 'uv' => 0, 'bandwidth_in' => 0, 'bandwidth_out' => 0]; + } + $daily[$today]['bandwidth_out'] += (int)$bytes; + @file_put_contents($dailyPath, json_encode($daily, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX); +} catch (Throwable $e) {} +respond(true, '保存成功', str_replace($_SERVER['DOCUMENT_ROOT'], '', $targetPath)); diff --git a/api/save_video.php b/api/save_video.php index 448b216..3aaee4b 100644 --- a/api/save_video.php +++ b/api/save_video.php @@ -77,6 +77,25 @@ if ($skipDownload) { } } +$bytes = @filesize($targetPath) ?: 0; +try { + $statsRoot = __DIR__ . '/../stats'; + if (!is_dir($statsRoot)) { @mkdir($statsRoot, 0777, true); } + $dailyPath = $statsRoot . '/daily.json'; + $today = (new DateTime('now'))->format('Y-m-d'); + $daily = []; + if (file_exists($dailyPath)) { + $rawDaily = @file_get_contents($dailyPath); + $decoded = json_decode($rawDaily, true); + if (is_array($decoded)) { $daily = $decoded; } + } + if (!isset($daily[$today]) || !is_array($daily[$today])) { + $daily[$today] = ['visits' => 0, 'video_plays' => 0, 'picture_shows' => 0, 'uv' => 0, 'bandwidth_in' => 0, 'bandwidth_out' => 0]; + } + $daily[$today]['bandwidth_out'] += (int)$bytes; + @file_put_contents($dailyPath, json_encode($daily, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX); +} catch (Throwable $e) {} + // 恢复清理逻辑:仅保留最近两天的视频目录,删除更早的目录 try { $dirs = @scandir($videosRoot); @@ -93,7 +112,7 @@ try { // 按日期升序排序 sort($dateDirs); // 保留最近两天(末尾两项),其它全部删除 - if (count($dateDirs) > 30) { + if (count($dateDirs) > 2) { $toDelete = array_slice($dateDirs, 0, count($dateDirs) - 2); foreach ($toDelete as $del) { rrmdir($videosRoot . DIRECTORY_SEPARATOR . $del); diff --git a/api/stats.php b/api/stats.php new file mode 100644 index 0000000..ad07684 --- /dev/null +++ b/api/stats.php @@ -0,0 +1,99 @@ +format('Y-m-d'); + +if ($method === 'GET') { + $daily = []; + if (file_exists($dailyPath)) { + $rawDaily = @file_get_contents($dailyPath); + $decoded = json_decode($rawDaily, true); + if (is_array($decoded)) { $daily = $decoded; } + } + $keys = array_keys($daily); + sort($keys); + $last7 = array_slice($keys, max(0, count($keys) - 7)); + $out = []; + foreach ($last7 as $k) { $out[$k] = $daily[$k]; } + $todayData = $daily[$today] ?? ['visits' => 0, 'video_plays' => 0, 'picture_shows' => 0, 'uv' => 0, 'bandwidth_in' => 0, 'bandwidth_out' => 0]; + echo json_encode(['success' => true, 'today' => $todayData, 'daily' => $out], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} + +$raw = file_get_contents('php://input'); +$data = json_decode($raw, true); +$event = isset($data['event']) ? trim((string)$data['event']) : ''; +$page = isset($data['page']) ? trim((string)$data['page']) : ''; +$url = isset($data['url']) ? trim((string)$data['url']) : ''; +$title = isset($data['title']) ? trim((string)$data['title']) : ''; +$allowed = ['visit','video_play','picture_show','bandwidth']; +if (!in_array($event, $allowed, true)) { + echo json_encode(['success' => false, 'message' => 'invalid event']); + exit; +} + +$daily = []; +if (file_exists($dailyPath)) { + $rawDaily = @file_get_contents($dailyPath); + $decoded = json_decode($rawDaily, true); + if (is_array($decoded)) { $daily = $decoded; } +} +if (!isset($daily[$today]) || !is_array($daily[$today])) { + $daily[$today] = ['visits' => 0, 'video_plays' => 0, 'picture_shows' => 0, 'uv' => 0, 'bandwidth_in' => 0, 'bandwidth_out' => 0]; +} +if ($event === 'visit') { $daily[$today]['visits']++; } +if ($event === 'video_play') { $daily[$today]['video_plays']++; } +if ($event === 'picture_show') { $daily[$today]['picture_shows']++; } + +if ($event === 'visit') { + $uvPath = $uvRoot . '/' . $today . '.json'; + $ips = []; + if (file_exists($uvPath)) { + $rawIps = @file_get_contents($uvPath); + $decodedIps = json_decode($rawIps, true); + if (is_array($decodedIps)) { $ips = $decodedIps; } + } + $ip = $_SERVER['REMOTE_ADDR'] ?? ''; + if ($ip !== '') { + if (!in_array($ip, $ips, true)) { $ips[] = $ip; } + } + @file_put_contents($uvPath, json_encode($ips, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX); + $daily[$today]['uv'] = count($ips); +} +@file_put_contents($dailyPath, json_encode($daily, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX); + +$eventsPath = $eventsRoot . '/' . $today . '.json'; +$events = []; +if (file_exists($eventsPath)) { + $rawEvents = @file_get_contents($eventsPath); + $decoded = json_decode($rawEvents, true); + if (is_array($decoded)) { $events = $decoded; } +} +$events[] = [ + 'type' => $event, + 'page' => $page, + 'url' => $url, + 'title' => $title, + 'direction' => $direction ?? '', + 'bytes' => $bytes ?? 0, + 'ip' => $_SERVER['REMOTE_ADDR'] ?? '', + 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '', + 'ts' => (new DateTime('now'))->format(DateTime::ATOM), +]; +@file_put_contents($eventsPath, json_encode($events, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX); + +echo json_encode(['success' => true, 'today' => $daily[$today]], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$direction = isset($data['direction']) ? (string)$data['direction'] : ''; +$bytes = isset($data['bytes']) ? intval($data['bytes']) : 0; +if ($event === 'bandwidth' && $bytes > 0) { + if ($direction === 'in') { $daily[$today]['bandwidth_in'] += $bytes; } + else if ($direction === 'out') { $daily[$today]['bandwidth_out'] += $bytes; } +} diff --git a/api/upload_picture.php b/api/upload_picture.php index 22a24d9..d19c188 100644 --- a/api/upload_picture.php +++ b/api/upload_picture.php @@ -71,6 +71,25 @@ if (!move_uploaded_file($file['tmp_name'], $target)) { exit; } +$bytes = (int)($file['size'] ?? 0); +try { + $statsRoot = __DIR__ . '/../stats'; + if (!is_dir($statsRoot)) { @mkdir($statsRoot, 0777, true); } + $dailyPath = $statsRoot . '/daily.json'; + $today = (new DateTime('now'))->format('Y-m-d'); + $daily = []; + if (file_exists($dailyPath)) { + $rawDaily = @file_get_contents($dailyPath); + $decoded = json_decode($rawDaily, true); + if (is_array($decoded)) { $daily = $decoded; } + } + if (!isset($daily[$today]) || !is_array($daily[$today])) { + $daily[$today] = ['visits' => 0, 'video_plays' => 0, 'picture_shows' => 0, 'uv' => 0, 'bandwidth_in' => 0, 'bandwidth_out' => 0]; + } + $daily[$today]['bandwidth_in'] += (int)$bytes; + @file_put_contents($dailyPath, json_encode($daily, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX); +} catch (Throwable $e) {} + $meta = [ 'original_name' => $origName, 'saved_name' => basename($target), diff --git a/api/upload_video.php b/api/upload_video.php index f1e62bf..3561e2f 100644 --- a/api/upload_video.php +++ b/api/upload_video.php @@ -73,6 +73,25 @@ if (!move_uploaded_file($file['tmp_name'], $target)) { exit; } +$bytes = (int)($file['size'] ?? 0); +try { + $statsRoot = __DIR__ . '/../stats'; + if (!is_dir($statsRoot)) { @mkdir($statsRoot, 0777, true); } + $dailyPath = $statsRoot . '/daily.json'; + $today = (new DateTime('now'))->format('Y-m-d'); + $daily = []; + if (file_exists($dailyPath)) { + $rawDaily = @file_get_contents($dailyPath); + $decoded = json_decode($rawDaily, true); + if (is_array($decoded)) { $daily = $decoded; } + } + if (!isset($daily[$today]) || !is_array($daily[$today])) { + $daily[$today] = ['visits' => 0, 'video_plays' => 0, 'picture_shows' => 0, 'uv' => 0, 'bandwidth_in' => 0, 'bandwidth_out' => 0]; + } + $daily[$today]['bandwidth_in'] += (int)$bytes; + @file_put_contents($dailyPath, json_encode($daily, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX); +} catch (Throwable $e) {} + // 可选:记录一个简单的元信息文件 $meta = [ 'original_name' => $origName, diff --git a/assets/script.js b/assets/script.js index 1d9fa84..861d98f 100644 --- a/assets/script.js +++ b/assets/script.js @@ -14,6 +14,44 @@ document.addEventListener('DOMContentLoaded', () => { const videoWrap = document.getElementById('videoWrap'); const videoPanel = document.getElementById('video'); const includeLocalToggle = document.getElementById('includeLocalToggle'); + const statFlowEl = document.getElementById('statFlow'); + const statVisitEl = document.getElementById('statVisit'); + const statVideoEl = document.getElementById('statVideo'); + const statPicEl = document.getElementById('statPic'); + const formatBytes = (n) => { + let val = Number(n) || 0; + const units = ['B','KB','MB','GB','TB','PB','EB','ZB','YB']; + let i = 0; + while (val >= 1024 && i < units.length - 1) { val /= 1024; i++; } + const digits = val >= 100 ? 0 : (val >= 10 ? 1 : 2); + return val.toFixed(digits) + ' ' + units[i]; + }; + const formatCount = (n) => { + if (n >= 100000000) return (n / 100000000).toFixed(n < 1000000000 ? 2 : 1) + ' 亿'; + if (n >= 10000) return (n / 10000).toFixed(n < 100000 ? 2 : 1) + ' 万'; + return new Intl.NumberFormat('zh-CN').format(n); + }; + const updateStatsUI = (t) => { + if (!t) return; + const bw = (t.bandwidth_in ?? 0) + (t.bandwidth_out ?? 0); + if (statFlowEl) statFlowEl.textContent = formatBytes(bw); + if (statVisitEl) statVisitEl.textContent = formatCount(t.visits ?? 0); + if (statVideoEl) statVideoEl.textContent = formatCount(t.video_plays ?? 0); + if (statPicEl) statPicEl.textContent = formatCount(t.picture_shows ?? 0); + }; + const track = async (payload) => { + try { + const r = await fetch('api/stats.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + const j = await r.json(); + if (j && j.success && j.today) updateStatsUI(j.today); + } catch {} + }; + (async () => { try { const r = await fetch('api/stats.php'); const j = await r.json(); if (j && j.success && j.today) updateStatsUI(j.today); } catch {} })(); + track({ event: 'visit', page: location.pathname }); // 底部工具栏与标题已移除,无需获取相关元素 if (!player || !videoWrap) return; // 仅在视频区存在时执行 @@ -332,6 +370,7 @@ document.addEventListener('DOMContentLoaded', () => { pictureImg.src = url; picLastSwap = Date.now(); if (galleryPanel) galleryPanel.style.setProperty('--pic-progress', '0'); + track({ event: 'picture_show', page: location.pathname, url }); if (pictureWrap.matches(':hover')) showPic(); } })(); @@ -341,6 +380,7 @@ document.addEventListener('DOMContentLoaded', () => { pictureImg.src = url; picLastSwap = Date.now(); if (galleryPanel) galleryPanel.style.setProperty('--pic-progress', '0'); + track({ event: 'picture_show', page: location.pathname, url }); if (pictureWrap.matches(':hover')) showPic(); } }, picIntervalMs); diff --git a/assets/style.css b/assets/style.css index c68bd0f..b991b02 100644 --- a/assets/style.css +++ b/assets/style.css @@ -178,6 +178,22 @@ body { } .menu__controls input[type="checkbox"] { vertical-align: middle; margin-right: 6px; } +.menu__stats { + display: flex; + align-items: center; + gap: 8px; + color: var(--red); +} +.menu__stats .stat { + background: #fff; + border: 1px solid var(--red); + border-radius: 999px; + padding: 6px 10px; + font-size: 12px; +} +.menu__stats .stat__label { opacity: .8; margin-right: 4px; } +.menu__stats .stat__value { font-weight: 600; } + video#player { width: 100%; flex: 1 1 auto; diff --git a/index.php b/index.php index 7e071de..ab76220 100644 --- a/index.php +++ b/index.php @@ -169,6 +169,12 @@ 包含本地 +