功能: - StarLight Yggdrasil 第三方登录(后端中转,accessToken 不落地) - 管理员可视化权限管理(用户列表 + 一键授予/移出) - 设置页热配置(11 项可编辑,立即生效) - 循环池(每流独立手动歌单,队列空时随机不重复抽取) - 登录页统一主站浅色风格 - 全站改名「Starlight专属电台」 - 删除关于页 安全: - 全局 CSRF 防护(双提交 Cookie) - 登录限流防 XFF 伪造、错误信息脱敏 - 权限中间件 requireAdminForWrites(读放行/写 admin) - 密码哈希存储(scrypt/sha256) - SSRF 防护(封面下载域名白名单) - 空 catch 加日志、SSE 连接上限 修复: - 多角色账号 selectedProfile 缺失 - 普通用户越权删歌(V4) - streams 写操作越权(V9) - 设置页布局溢出 测试:38 项全绿(api16 / e2e17 / write5)
458 lines
23 KiB
HTML
458 lines
23 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>Starlight专属电台</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>Starlight专属电台</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
|
||
>
|
||
<span class="user-name" id="userName" title="" style="display:none"></span>
|
||
<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-sliders" style="color: var(--accent)"></i>
|
||
热配置
|
||
</div>
|
||
<button class="btn-clear" id="btnSaveSettings"><i class="bi bi-check-lg"></i> 保存并生效</button>
|
||
</div>
|
||
<div class="section-body">
|
||
<div class="settings-form" id="settingsForm">
|
||
<div class="setting-row">
|
||
<label>切片时长(秒)</label>
|
||
<input type="number" id="cfgHlsTime" min="2" max="30" placeholder="5" value="5">
|
||
</div>
|
||
<div class="setting-row">
|
||
<label>滑动窗口(段)</label>
|
||
<input type="number" id="cfgSlideWindow" min="5" max="60" placeholder="15" value="15">
|
||
</div>
|
||
<div class="setting-row">
|
||
<label>缓存上限(MB)</label>
|
||
<input type="number" id="cfgMaxCacheMB" min="100" max="100000" placeholder="1000" value="1000">
|
||
</div>
|
||
<div class="setting-row">
|
||
<label>默认频道</label>
|
||
<select id="cfgDefaultStream"></select>
|
||
</div>
|
||
<div class="setting-row">
|
||
<label>日志级别</label>
|
||
<select id="cfgLogLevel">
|
||
<option value="debug">debug</option>
|
||
<option value="info">info</option>
|
||
<option value="warn">warn</option>
|
||
<option value="error">error</option>
|
||
</select>
|
||
</div>
|
||
<div class="setting-row">
|
||
<label>会话有效期(小时)</label>
|
||
<input type="number" id="cfgSessionTtlHours" min="1" max="720" placeholder="72" value="72">
|
||
</div>
|
||
<div class="setting-row">
|
||
<label>登录失败上限(次)</label>
|
||
<input type="number" id="cfgLoginMaxFails" min="3" max="20" placeholder="5" value="5">
|
||
</div>
|
||
<div class="setting-row">
|
||
<label>登录锁定时长(分)</label>
|
||
<input type="number" id="cfgLoginLockMin" min="1" max="1440" placeholder="10" value="10">
|
||
</div>
|
||
<div class="setting-row">
|
||
<label>搜索缓存时长(秒)</label>
|
||
<input type="number" id="cfgSearchCacheTtlSec" min="10" max="3600" placeholder="300" value="300">
|
||
</div>
|
||
<div class="setting-row">
|
||
<label>搜索限流上限(次/窗口)</label>
|
||
<input type="number" id="cfgSearchRateMax" min="5" max="500" placeholder="20" value="20">
|
||
</div>
|
||
<div class="setting-row">
|
||
<label>搜索限流窗口(秒)</label>
|
||
<input type="number" id="cfgSearchRateWindowSec" min="10" max="600" placeholder="60" value="60">
|
||
</div>
|
||
</div>
|
||
<div class="cache-stat-note" style="margin-top:12px">
|
||
ⓘ 修改后点「保存并生效」,立即应用,无需重启。切片时长等影响新生成的流。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<div class="section-title">
|
||
<i class="bi bi-shield-lock" style="color: var(--accent)"></i>
|
||
权限管理
|
||
</div>
|
||
<button class="btn-clear" id="btnRefreshUsers"><i class="bi bi-arrow-repeat"></i> 刷新</button>
|
||
</div>
|
||
<div class="section-body">
|
||
<div class="cache-stat-note" style="margin-bottom:12px">
|
||
ⓘ 登录过的用户列表。点「设为管理员」授予权限(下次登录生效)。
|
||
</div>
|
||
<div class="user-list" id="userList">加载中...</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>
|
||
|
||
</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>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- Global audio element -->
|
||
<audio id="musicPlayer" preload="none" style="display: none"></audio>
|
||
|
||
<script src="js/app.js"></script>
|
||
</body>
|
||
</html>
|