- POST /api/music/refresh-covers (requireAdmin) - 设置页封面维护卡片 + 前端事件 - 真实场景验证:清空封面后可刷回
529 lines
25 KiB
HTML
529 lines
25 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta
|
||
name="viewport"
|
||
content="width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=1"
|
||
/>
|
||
<meta name="theme-color" content="#f4f5f9" />
|
||
<title>Xiao の 电台</title>
|
||
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
|
||
<link rel="stylesheet" href="vendor/bootstrap.min.css" />
|
||
<link rel="stylesheet" href="vendor/bootstrap-icons.css" />
|
||
<script src="vendor/Sortable.min.js"></script>
|
||
<script src="vendor/hls.min.js"></script>
|
||
<link rel="stylesheet" href="css/style.css" />
|
||
</head>
|
||
<body>
|
||
<div class="page-bg"></div>
|
||
<div class="bg-shapes">
|
||
<div class="s"></div>
|
||
<div class="s"></div>
|
||
<div class="s"></div>
|
||
</div>
|
||
<div class="toast-container" id="toastContainer"></div>
|
||
|
||
<!-- Header -->
|
||
<header class="top-header">
|
||
<div class="header-inner">
|
||
<div class="brand">
|
||
<div class="brand-icon">
|
||
<img src="favicon.png" alt="logo" />
|
||
</div>
|
||
<span>Xiao の 电台</span>
|
||
<span class="live-pill"
|
||
><span class="live-dot"></span>LIVE</span
|
||
>
|
||
</div>
|
||
<div class="header-actions">
|
||
<span class="cache-pill" id="cacheInfo"><i class="bi bi-database"></i> --</span>
|
||
<button class="channel-btn" id="btnStreamMgr" title="频道与切换"><i class="bi bi-broadcast"></i><span id="currentChannelName">主电台</span></button>
|
||
<button class="btn-link-sm" id="liveLink" title="复制直播流链接"
|
||
><i class="bi bi-link-45deg"></i></button
|
||
>
|
||
<button class="btn-link-sm" id="btnLogout" title="退出登录"
|
||
><i class="bi bi-box-arrow-right"></i></button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Pages -->
|
||
<div class="page-container">
|
||
<!-- 0. Live 收听 -->
|
||
<div class="page active" id="pageLive">
|
||
<div class="live-card">
|
||
<div class="live-now-playing">
|
||
<div class="live-cover-wrap">
|
||
<img class="live-cover" id="liveCover" src="" alt="" />
|
||
<div class="live-cover-ph" id="liveCoverPh">
|
||
<i class="bi bi-broadcast"></i>
|
||
</div>
|
||
<span class="live-badge"><span class="live-dot"></span>LIVE</span>
|
||
</div>
|
||
<div class="live-meta">
|
||
<div class="live-station" id="liveStation">主电台</div>
|
||
<div class="live-now" id="liveNow">正在加载直播流...</div>
|
||
<div class="live-sub" id="liveSub">--</div>
|
||
</div>
|
||
</div>
|
||
<div class="live-controls">
|
||
<button class="live-play-btn" id="livePlayBtn"><i class="bi bi-play-fill"></i></button>
|
||
<div class="live-progress-wrap">
|
||
<div class="live-progress-bar"><div class="live-progress-fill" id="liveProgressFill"></div></div>
|
||
<div class="live-status" id="liveStatus">未连接</div>
|
||
</div>
|
||
</div>
|
||
<div class="live-hint">
|
||
<i class="bi bi-info-circle"></i>
|
||
点击播放按钮收听 <b id="liveHintStation">主电台</b> 的实时直播流
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 1. Search -->
|
||
<div class="page" id="pageSearch">
|
||
<div class="search-card">
|
||
<div class="search-header">
|
||
<h2>导入音源</h2>
|
||
<p>输入网易云歌曲 ID、链接或歌名查询并下载</p>
|
||
</div>
|
||
<div class="search-input-wrap">
|
||
<div class="search-field">
|
||
<input
|
||
type="text"
|
||
id="musicId"
|
||
placeholder="歌曲 ID、链接或歌名,如 33894312 / 知我"
|
||
inputmode="text"
|
||
autocomplete="off"
|
||
spellcheck="false"
|
||
/>
|
||
<button class="btn-search" id="btnSearch">
|
||
查询
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="parse-hint" id="parseHint">
|
||
<i class="bi bi-magic"></i>
|
||
已自动解析为 ID:<code id="parsedId"></code>
|
||
</div>
|
||
<!-- Preview: info only -->
|
||
<div id="musicPreview" style="display: none">
|
||
<div class="preview-card">
|
||
<div class="preview-cover-section">
|
||
<div class="preview-cover-wrap">
|
||
<img
|
||
id="musicCover"
|
||
class="preview-cover"
|
||
src=""
|
||
alt=""
|
||
/>
|
||
</div>
|
||
<div class="preview-info">
|
||
<div
|
||
class="preview-title"
|
||
id="musicName"
|
||
></div>
|
||
<div
|
||
class="preview-artist"
|
||
id="musicArtist"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
class="preview-details"
|
||
id="musicDetails"
|
||
></div>
|
||
<div class="preview-actions">
|
||
<button class="btn-download" id="btnDownload">
|
||
<i class="bi bi-download"></i> 下载到资源库
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
id="searchResults"
|
||
class="searchResults"
|
||
style="display: none"
|
||
></div>
|
||
<div class="search-tips" id="searchTips">
|
||
<div class="tips-icon">
|
||
<i class="bi bi-music-note-beamed"></i>
|
||
</div>
|
||
<div class="tips-title">搜索并下载音乐</div>
|
||
<div class="tips-desc">
|
||
在网易云音乐中复制歌曲 ID 或完整链接<br />粘贴到上方输入框即可自动识别
|
||
</div>
|
||
</div>
|
||
<!-- Import local audio -->
|
||
<div class="import-card" id="importCard">
|
||
<div class="import-header">
|
||
<h3><i class="bi bi-upload"></i> 本地导入</h3>
|
||
<p>上传音频文件,自动识别作者与封面(封面存本地)</p>
|
||
</div>
|
||
<input type="file" id="importFile" accept="audio/*,.mp3,.m4a,.flac,.wav,.aac,.ogg" hidden />
|
||
<input type="text" id="importTitle" class="import-input" placeholder="歌曲名(选填,留空则用文件名)" />
|
||
<input type="text" id="importArtist" class="import-input" placeholder="作者(选填,留空自动匹配)" />
|
||
<button class="btn-download" id="btnImport"><i class="bi bi-upload"></i> 选择文件并导入</button>
|
||
<div class="import-progress" id="importProgress"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 2. Resources -->
|
||
<div class="page" id="pageResources">
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<div class="section-title">
|
||
<i
|
||
class="bi bi-folder2-open"
|
||
style="color: var(--accent)"
|
||
></i>
|
||
已有音乐
|
||
<span class="section-count" id="resourceCount"
|
||
>0</span
|
||
>
|
||
</div>
|
||
<div class="resource-tools">
|
||
<select class="resource-sort" id="resourceSort" title="排序方式">
|
||
<option value="created_desc">加入时间 ↓</option>
|
||
<option value="created_asc">加入时间 ↑</option>
|
||
<option value="heat_desc">热度 ↓</option>
|
||
<option value="heat_asc">热度 ↑</option>
|
||
<option value="name_asc">名称 ↑</option>
|
||
<option value="name_desc">名称 ↓</option>
|
||
</select>
|
||
<input
|
||
type="text"
|
||
class="resource-search"
|
||
id="resourceSearch"
|
||
placeholder="搜索已有音乐..."
|
||
autocomplete="off"
|
||
spellcheck="false"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="section-body" id="resourceList">
|
||
<div class="empty-state">
|
||
<div class="empty-icon">
|
||
<i class="bi bi-arrow-repeat"></i>
|
||
</div>
|
||
<div class="empty-title">加载中...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 3. Playlist -->
|
||
<div class="page" id="pagePlaylist">
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<div class="section-title">
|
||
<i
|
||
class="bi bi-music-note-list"
|
||
style="color: var(--accent)"
|
||
></i>
|
||
播放列表
|
||
<span class="section-count" id="playlistCount"
|
||
>0</span
|
||
>
|
||
</div>
|
||
<button class="btn-clear" id="btnClear">
|
||
<i class="bi bi-trash3"></i> 清空
|
||
</button>
|
||
</div>
|
||
<div class="section-body" id="playlistList">
|
||
<div class="empty-state">
|
||
<div class="empty-icon">
|
||
<i class="bi bi-arrow-repeat"></i>
|
||
</div>
|
||
<div class="empty-title">加载中...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 5. Settings -->
|
||
<div class="page" id="pageSettings">
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<div class="section-title">
|
||
<i class="bi bi-database" style="color: var(--warning)"></i>
|
||
缓存管理
|
||
</div>
|
||
<button class="btn-clear" id="btnCleanup"><i class="bi bi-trash3"></i> 立即清理</button>
|
||
</div>
|
||
<div class="section-body">
|
||
<div class="cache-stat">
|
||
<div class="cache-stat-row">
|
||
<span>已用 / 上限</span>
|
||
<b id="setCacheText">-- / --</b>
|
||
</div>
|
||
<div class="cache-bar"><div class="cache-bar-fill" id="setCacheBar"></div></div>
|
||
<div class="cache-stat-note" id="setCacheNote">超限时自动按热度 LRU 淘汰</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<div class="section-title">
|
||
<i class="bi bi-image" style="color: var(--accent)"></i>
|
||
封面维护
|
||
</div>
|
||
<button class="btn-clear" id="btnRefreshCovers"><i class="bi bi-arrow-repeat"></i> 刷新缺失封面</button>
|
||
</div>
|
||
<div class="section-body">
|
||
<div class="cache-stat-note" id="coverRefreshNote">为所有没有封面的音乐重新匹配网易云封面(远程优先,本地兜底)</div>
|
||
<div class="import-progress" id="coverRefreshProgress"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<div class="section-title">
|
||
<i class="bi bi-gear" style="color: var(--accent)"></i>
|
||
运行配置
|
||
</div>
|
||
</div>
|
||
<div class="section-body">
|
||
<div class="config-grid" id="setConfigGrid"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<div class="section-title">
|
||
<i class="bi bi-file-text" style="color: var(--success)"></i>
|
||
运行日志
|
||
</div>
|
||
<select class="log-tail-select" id="logTail">
|
||
<option value="50">50 行</option>
|
||
<option value="100" selected>100 行</option>
|
||
<option value="200">200 行</option>
|
||
<option value="500">500 行</option>
|
||
</select>
|
||
</div>
|
||
<div class="section-body">
|
||
<pre class="log-view" id="logView">加载中...</pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 4. About — 保持原有风格体系 -->
|
||
<div class="page" id="pageAbout">
|
||
<div class="about-card">
|
||
<!-- Hero -->
|
||
<div class="about-hero">
|
||
<div class="about-logo">
|
||
<img src="favicon.png" alt="logo" />
|
||
</div>
|
||
<h2>Xiao の 电台</h2>
|
||
<p>基于 HLS 的个人在线音乐电台</p>
|
||
</div>
|
||
|
||
<!-- Body -->
|
||
<div class="about-body">
|
||
<!-- 项目简介 -->
|
||
<div class="about-section">
|
||
<div class="about-section-icon">
|
||
<i class="bi bi-book"></i>
|
||
</div>
|
||
<div class="about-section-content">
|
||
<h3>项目简介</h3>
|
||
<p>
|
||
一个基于 Web 的个人在线音乐电台,支持通过网易云音乐歌曲 ID、链接或歌名进行搜索、试听与下载,提供 HLS 直播流播放、资源管理与播放列表拖拽排序等功能。界面采用简约玻璃拟态风格,适配移动端与桌面端多种屏幕尺寸。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 核心功能 -->
|
||
<div class="about-section">
|
||
<div class="about-section-icon">
|
||
<i class="bi bi-lightning-charge"></i>
|
||
</div>
|
||
<div class="about-section-content">
|
||
<h3>核心功能</h3>
|
||
<div class="about-feature-list">
|
||
<div class="about-feat">
|
||
<i class="bi bi-search" style="color: var(--accent)"></i>
|
||
<span>智能搜索 — 歌曲 ID、链接或歌名关键词,自动解析并一键导入</span>
|
||
</div>
|
||
<div class="about-feat">
|
||
<i class="bi bi-broadcast" style="color: var(--success)"></i>
|
||
<span>直播流 — FFmpeg HLS 切片 + 滑动窗口,5 秒精度连续输出</span>
|
||
</div>
|
||
<div class="about-feat">
|
||
<i class="bi bi-database-check" style="color: var(--warning)"></i>
|
||
<span>智能缓存 — 预生成切片、增量追踪、LRU 淘汰与自动清理</span>
|
||
</div>
|
||
<div class="about-feat">
|
||
<i class="bi bi-hand-index-thumb" style="color: #8b5cf6"></i>
|
||
<span>播放列表 — 拖拽排序、添加/移除/跳转,SSE 实时状态推送</span>
|
||
</div>
|
||
<div class="about-feat">
|
||
<i class="bi bi-phone" style="color: #06b6d4"></i>
|
||
<span>全端适配 — 玻璃拟态 UI,手势滑动、键盘快捷键、拖拽进度条</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 技术栈 -->
|
||
<div class="about-section">
|
||
<div class="about-section-icon">
|
||
<i class="bi bi-tools"></i>
|
||
</div>
|
||
<div class="about-section-content">
|
||
<h3>技术栈</h3>
|
||
<div class="tech-tags">
|
||
<span>HTML5</span>
|
||
<span>CSS3</span>
|
||
<span>JavaScript ES6+</span>
|
||
<span>Bootstrap 5</span>
|
||
<span>SortableJS</span>
|
||
<span>HLS.js</span>
|
||
<span>Node.js</span>
|
||
<span>Express</span>
|
||
<span>FFmpeg</span>
|
||
<span>Chokidar</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 开发语言 -->
|
||
<div class="about-section">
|
||
<div class="about-section-icon">
|
||
<i class="bi bi-translate"></i>
|
||
</div>
|
||
<div class="about-section-content">
|
||
<h3>开发语言</h3>
|
||
<p>前端:HTML / CSS / JavaScript | 后端:Node.js</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 作者 -->
|
||
<div class="about-section">
|
||
<div class="about-section-icon">
|
||
<i class="bi bi-person"></i>
|
||
</div>
|
||
<div class="about-section-content">
|
||
<h3>作者</h3>
|
||
<p style="margin-bottom: 0.45rem">Xiao — 独立开发者</p>
|
||
<a
|
||
href="https://github.com/Xiao-no-love/"
|
||
target="_blank"
|
||
rel="noopener"
|
||
class="about-gh-link"
|
||
>
|
||
<i class="bi bi-github"></i>
|
||
<span>Xiao-no-love</span>
|
||
<i class="bi bi-box-arrow-up-right" style="font-size: 0.6rem; opacity: 0.5"></i>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 免责声明 -->
|
||
<div class="about-section disclaimer-section">
|
||
<div class="about-section-icon">
|
||
<i class="bi bi-exclamation-triangle"></i>
|
||
</div>
|
||
<div class="about-section-content">
|
||
<h3>免责声明</h3>
|
||
<p>
|
||
本项目所有音源均来自网易云音乐,仅供个人学习与技术研究使用,严禁用于任何商业用途。音乐版权归原创作者及网易云音乐平台所有。如有侵权,请联系删除。请于下载后 24 小时内清除,支持正版音乐。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div class="about-version">
|
||
v2.0.0 · Built with ♥ ·
|
||
<a
|
||
href="https://github.com/Xiao-no-love/"
|
||
target="_blank"
|
||
rel="noopener"
|
||
style="color: var(--accent); text-decoration: none"
|
||
>GitHub</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 频道管理模态框 -->
|
||
<div class="modal-mask" id="streamModal">
|
||
<div class="modal-box">
|
||
<div class="modal-head">
|
||
<h3><i class="bi bi-sliders"></i> 频道管理</h3>
|
||
<button class="modal-close" id="streamModalClose" title="关闭"><i class="bi bi-x-lg"></i></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="stream-list" id="streamMgrList"></div>
|
||
<div class="stream-create">
|
||
<input type="text" id="newStreamId" placeholder="频道 ID(字母数字_-,如 chill)" autocomplete="off" spellcheck="false" />
|
||
<input type="text" id="newStreamName" placeholder="显示名称(选填)" autocomplete="off" />
|
||
<button id="btnCreateStream"><i class="bi bi-plus-lg"></i> 新建频道</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Mini Player — full background progress + drag seek -->
|
||
<div class="mini-player" id="miniPlayer">
|
||
<div class="mp-progress" id="mpProgress"></div>
|
||
<div class="mp-body" id="mpBody">
|
||
<img
|
||
class="mp-cover"
|
||
id="mpCover"
|
||
src=""
|
||
alt=""
|
||
onerror="this.style.display = 'none'"
|
||
/>
|
||
<div class="mp-info">
|
||
<div class="mp-name" id="mpName">未在播放</div>
|
||
<div class="mp-artist" id="mpArtist">--</div>
|
||
</div>
|
||
<div class="mp-controls">
|
||
<span class="mp-time" id="mpTime">0:00</span>
|
||
<button class="mp-play" id="mpPlay">
|
||
<i class="bi bi-play-fill"></i>
|
||
</button>
|
||
<button class="mp-expand" id="mpExpand" title="查看详情">
|
||
<i class="bi bi-chevron-up"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bottom Nav — 4 tabs, full-width aligned with content -->
|
||
<nav class="bottom-nav">
|
||
<div class="bottom-nav-inner">
|
||
<button class="nav-tab active" data-page="pageLive">
|
||
<i class="bi bi-broadcast"></i><span>收听</span>
|
||
</button>
|
||
<button class="nav-tab" data-page="pageSearch">
|
||
<i class="bi bi-search"></i><span>搜索</span>
|
||
</button>
|
||
<button class="nav-tab" data-page="pageResources">
|
||
<i class="bi bi-folder"></i><span>资源</span>
|
||
</button>
|
||
<button class="nav-tab" data-page="pagePlaylist">
|
||
<i class="bi bi-list-ul"></i><span>列表</span>
|
||
</button>
|
||
<button class="nav-tab" data-page="pageSettings">
|
||
<i class="bi bi-gear"></i><span>设置</span>
|
||
</button>
|
||
<button class="nav-tab" data-page="pageAbout">
|
||
<i class="bi bi-info-circle"></i><span>关于</span>
|
||
</button>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- Global audio element -->
|
||
<audio id="musicPlayer" preload="none" style="display: none"></audio>
|
||
|
||
<script src="js/app.js"></script>
|
||
</body>
|
||
</html>
|