From fa6eeb25b992e8ea1a9599664e221a32204350a4 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 24 Sep 2026 19:46:35 +0800 Subject: [PATCH] =?UTF-8?q?baseline:=20=E9=9F=B3=E4=B9=90=E5=9C=A8?= =?UTF-8?q?=E7=BA=BF=E7=94=B5=E5=8F=B0=20v1=EF=BC=88=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=89=8D=E5=9F=BA=E7=BA=BF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 20 + .gitignore | 35 + README.md | 252 ++++++ index.js | 1233 +++++++++++++++++++++++++++++ package-lock.json | 1022 ++++++++++++++++++++++++ package.json | 16 + public/css/style.css | 1750 ++++++++++++++++++++++++++++++++++++++++++ public/favicon.ico | Bin 0 -> 270398 bytes public/favicon.png | Bin 0 -> 98344 bytes public/index.html | 397 ++++++++++ public/js/app.js | 758 ++++++++++++++++++ 11 files changed, 5483 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/css/style.css create mode 100644 public/favicon.ico create mode 100644 public/favicon.png create mode 100644 public/index.html create mode 100644 public/js/app.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e274ec6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# EditorConfig — 统一跨编辑器代码风格 +# https://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.{json,jsonc}] +indent_size = 2 + +[*.html] +indent_size = 4 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a1b9b63 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# 依赖 +node_modules/ + +# 运行时生成的 HLS 产物(可随时重建) +hls/ +hls_cache/ + +# 运行时数据(数据库) +*.db +*.db-wal +*.db-shm + +# 旧的 JSON 数据(已废弃) +playlist.json +heat.json +metadata.json + +# 本地化封面与第三方前端资源 +public/covers/ +public/vendor/ + +# 上传临时目录 +tmp/ +uploads/ + +# 日志与临时文件 +*.log +npm-debug.log* +*.tmp + +# 编辑器 / 系统 +.vscode/ +.idea/ +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..de14c0c --- /dev/null +++ b/README.md @@ -0,0 +1,252 @@ +# 音乐在线电台 + +把 `audio/` 目录里的歌曲用 FFmpeg 切成 HLS 切片,拼成一条永不停播的直播流,并提供 Web 控制面板管理播放列表、从网易云导入音源。前后端一体的 Node.js 应用,无数据库依赖。 + +## 功能特性 + +- 🎵 音频文件自动转码切片(HLS),拼成 24 小时不间断的直播流 +- 📋 拖拽排序的播放列表(支持添加、移除、清空、切歌) +- 🔥 热度统计 + 缓存自动清理(上限 1GB,按热度淘汰) +- ☁️ 网易云音源导入:输入歌曲 ID,一键下载入库并显示封面/歌名/作者 +- 🖼️ 资源卡片展示:封面图高斯模糊背景 + 时长/大小信息(hover 显示) +- 🔒 可选 API 鉴权(Bearer Token) + +## 环境要求 + +| 依赖 | 版本要求 | +|------|----------| +| Node.js | >= 14.14 | +| npm | 随 Node 附带 | +| FFmpeg | 任意较新版本(需包含 `ffmpeg` 与 `ffprobe`,且在系统 PATH 中) | + +> ⚠️ FFmpeg 是硬性依赖。缺少它程序能启动但无法转码切片,直播流不会有内容。安装后请在终端运行 `ffmpeg -version` 与 `ffprobe -version` 确认可用。 + +## 安装 + +### 1. 安装 Node.js + +**Windows** + +- 前往 下载 LTS 版本安装包(`.msi`),一路下一步即可。 +- 安装完成后重新打开终端,运行 `node -v` 应显示版本号(≥ v14.14)。 + +**Linux(Debian/Ubuntu 示例)** + +```bash +# 使用 NodeSource 源安装新版 Node +curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - +sudo apt-get install -y nodejs +node -v +``` + +> 其它发行版请参考 Node.js 官方安装文档,或使用包管理器安装 `nodejs` / `npm`。 + +### 2. 安装 FFmpeg + +**Windows(推荐)** + +方式一:用 winget(Windows 11 自带): + +```powershell +winget install Gyan.FFmpeg +``` + +方式二:手动下载 + +1. 打开 ,下载 `release essentials` 的 zip 包。 +2. 解压到固定目录(如 `C:\ffmpeg`)。 +3. 把解压后 **`bin` 子目录**的完整路径(如 `C:\ffmpeg\bin`)加入系统环境变量 `Path`: + - `Win` 键 → 搜索「编辑系统环境变量」→ 「环境变量」→ 双击「Path」→ 新建 → 粘贴路径 → 确定。 +4. **重新打开终端**,运行 `ffmpeg -version` 验证。 + +方式三:用 Chocolatey / Scoop: + +```powershell +choco install ffmpeg # 或 +scoop install ffmpeg +``` + +**Linux** + +```bash +# Debian / Ubuntu +sudo apt update && sudo apt install -y ffmpeg + +# CentOS / RHEL / Fedora +sudo dnf install -y ffmpeg # 部分发行版需先启用 EPEL 或 RPM Fusion 源 + +# Arch +sudo pacman -S ffmpeg +``` + +验证安装: + +```bash +ffmpeg -version +ffprobe -version +``` + +### 3. 安装项目依赖 + +在项目根目录(`demo/`)下: + +```bash +npm install +``` + +## 启动 + +```bash +npm start +``` + +启动成功后控制台会输出: + +``` +[INFO] 资源: N 个文件 +[CACHE] 启动扫描: ... +[LIVE] 已启动 | 切片 5s | 窗口 15 段 ≈75s | ... +[HTTP] 面板: http://127.0.0.1:3000 +[HTTP] 直播: http://127.0.0.1:3000/hls/index.m3u8 +``` + +- **控制面板**:浏览器打开 +- **直播流地址**:(可用 VLC、PotPlayer、ffplay 等播放器打开) + +停止服务:在终端按 `Ctrl + C`。 + +> 💡 Windows 下如果端口被旧进程占用导致启动失败(`EADDRINUSE`),见下文「常见问题」。 + +## 使用指南 + +### 添加音频 + +把音频文件(支持 `.mp3` / `.m4a` / `.flac` / `.wav`)直接放进 `audio/` 目录。程序用 chokidar 监听目录,**无需重启**,新文件会自动扫描并开始转码切片。 + +> 文件名建议用「作者 - 歌名.mp3」格式,程序会据此自动解析出作者与歌名,用于面板展示。 + +### 网易云音源导入 + +1. 在控制面板左侧「资源列表」顶部输入框填入网易云歌曲 ID。 +2. 点「查询」,预览区会显示封面、歌名、作者。 +3. 点「下载」,音频会自动保存到 `audio/` 并入库切片,封面/歌名/作者写入元数据。 + +**网易云歌曲 ID 在哪找?** 打开网易云网页版某首歌的页面,地址栏 `https://music.163.com/song?id=33894312` 中 `id=` 后面的数字即为歌曲 ID。 + +> ⚠️ 版权限制:只有免费、可播的歌曲能下载。VIP / 灰色无版权歌曲会被拦截并提示「无版权或需 VIP」,这是网易云服务端的限制,无法绕过。 + +### 管理播放列表 + +控制面板右侧「播放列表」支持: + +- ➕ 点资源卡片右侧的加号把歌曲加入播放列表 +- 🖱️ 拖拽排序(SortableJS) +- ❌ 移除单曲 +- 🗑️ 清空列表(清空后自动随机补一首,保证直播不断流) +- 歌曲播完自动从列表移除,列表空了自动随机补一首 + +播放列表与热度会持久化到磁盘,重启后保留。 + +## 目录结构与数据文件 + +``` +demo/ +├── index.js # 主程序(后端 + HLS 直播 + HTTP API) +├── package.json # 依赖与启动脚本 +├── public/ # 前端控制面板(静态文件,含 favicon) +│ ├── index.html +│ └── favicon.png +├── audio/ # 【你的音频】放这里,程序自动监听 +└── (以下为运行时自动生成,可随时删除,重启自动重建) + ├── hls/ # 直播流滑动窗口 index.m3u8 + ├── hls_cache/ # 每首歌的 HLS 切片缓存(上限 1GB,自动清理) + ├── heat.json # 歌曲热度(缓存清理优先级) + ├── playlist.json # 播放列表(持久化) + └── metadata.json # 歌曲元数据(歌名/作者/封面图/时长) +``` + +## 环境变量 + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `AUTH_TOKEN` | (空) | 设置后,所有 `/api/*` 接口需携带请求头 `Authorization: Bearer ` | +| `USE_DISCONTINUITY` | `false` | 设为 `true` 时在歌曲切换处插入 `#EXT-X-DISCONTINUITY` 标签(兼容性更好则保持关闭) | +| `PORT` | `3000` | HTTP 服务端口 | +| `HLS_TIME` | `5` | 单个 HLS 切片时长(秒),同时决定直播 tick 间隔 | +| `SLIDE_WINDOW` | `15` | 直播滑动窗口段数(≈ 窗口秒数 = HLS_TIME × 该值) | +| `MAX_CACHE_MB` | `1000` | HLS 缓存上限(MB),超出按热度淘汰 | +| `FFMPEG_CONCURRENCY` | `3` | FFmpeg 转码并发数 | +| `MAX_RETRIES` | `3` | 单文件转码失败重试上限 | +| `FFMPEG_TIMEOUT_MS` | `300000` | 单次 FFmpeg 转码超时(毫秒) | + +设置方式(三平台): + +**Windows PowerShell** + +```powershell +$env:AUTH_TOKEN="your_token"; npm start +``` + +**Windows CMD** + +```cmd +set AUTH_TOKEN=your_token && npm start +``` + +**Linux / macOS** + +```bash +AUTH_TOKEN=your_token npm start +``` + +## 常见问题 + +### 端口 3000 被占用 + +**Windows** + +```powershell +# 找到占用 3000 端口的 PID +netstat -ano | findstr :3000 +# 结束该进程(把 换成上一条命令查到的数字) +taskkill /PID /F +``` + +**Linux** + +```bash +# 查看占用进程 +sudo lsof -i :3000 +# 或 +sudo ss -tlnp | grep 3000 +# 结束进程(把 换成实际值) +kill +``` + +### 启动成功但直播流没声音 / 一直缓冲 + +1. 确认 `audio/` 目录里确有音频文件。 +2. 确认 FFmpeg 已正确安装并在 PATH 中:`ffmpeg -version`。 +3. 看启动日志里 `[PRESEG]` 是否报错——若某首歌转码失败会打印 `[PRESEG] 失败`,可检查该文件是否损坏。 +4. 首次启动需对所有歌曲转码,耗时随歌曲数量与长度增加,请耐心等待日志出现 `[PRESEG] 完成`。 + +### 网易云下载报「无版权或需 VIP」 + +正常现象,见上文「网易云音源导入」的版权说明。换一首免费歌曲即可。 + +### 局域网内其它设备访问不到 + +- 确认目标设备能 ping 通本机 IP。 +- **Windows**:需在防火墙中放行 Node.js(或放行 3000 端口)。首次启动时若弹出防火墙授权,点「允许」;否则在「Windows 安全中心 → 防火墙 → 允许应用通过防火墙」中手动添加 node.exe。 +- **Linux**:若有 `ufw` / `firewalld` 需放行端口: + +```bash +sudo ufw allow 3000 # ufw +sudo firewall-cmd --add-port=3000/tcp --permanent && sudo firewall-cmd --reload # firewalld +``` + +然后用本机局域网 IP 访问:`http://<本机IP>:3000`。 + +## 技术栈 + +Node.js · Express · Chokidar · FFmpeg(HLS 切片)· SortableJS · Bootstrap Icons diff --git a/index.js b/index.js new file mode 100644 index 0000000..a8c6789 --- /dev/null +++ b/index.js @@ -0,0 +1,1233 @@ +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); +const http = require("http"); +const https = require("https"); +const { spawn } = require("child_process"); +const chokidar = require("chokidar"); +const express = require("express"); + +// ==================== 路径常量 ==================== +const AUDIO_DIR = path.join(__dirname, "audio"); +const HLS_CACHE_DIR = path.join(__dirname, "hls_cache"); +const HLS_OUTPUT_DIR = path.join(__dirname, "hls"); +const PLAYLIST_FILE = path.join(__dirname, "playlist.json"); +const HEAT_FILE = path.join(__dirname, "heat.json"); +const METADATA_FILE = path.join(__dirname, "metadata.json"); +const PUBLIC_DIR = path.join(__dirname, "public"); + +for (const dir of [AUDIO_DIR, HLS_CACHE_DIR, HLS_OUTPUT_DIR, PUBLIC_DIR]) { + fs.mkdirSync(dir, { recursive: true }); +} + +// ==================== 配置 ==================== +// 环境变量解析助手:非法或缺失时回退默认值 +function envInt(name, fallback) { + const v = parseInt(process.env[name], 10); + return Number.isFinite(v) && v > 0 ? v : fallback; +} + +const MAX_CACHE_SIZE = envInt("MAX_CACHE_MB", 1000) * 1024 * 1024; +const HLS_TIME = envInt("HLS_TIME", 5); +const SLIDE_WINDOW_SIZE = envInt("SLIDE_WINDOW", 15); +const LIVE_TICK_INTERVAL = HLS_TIME * 1000; // 与切片时长对齐,避免累积漂移 +const HTTP_PORT = envInt("PORT", 3000); +const MAX_FFMPEG_CONCURRENT = envInt("FFMPEG_CONCURRENCY", 3); +const MAX_RETRIES = envInt("MAX_RETRIES", 3); +const FFMPEG_TIMEOUT = envInt("FFMPEG_TIMEOUT_MS", 5 * 60 * 1000); +const AUTH_TOKEN = process.env.AUTH_TOKEN || ""; + +// [FIX] DISCONTINUITY 默认关闭 +// 设为 true:严格合规,但部分播放器会在标签处插入静音 +// 设为 false:让播放器自然处理 PTS 跳变,兼容性更好 +const USE_DISCONTINUITY = process.env.USE_DISCONTINUITY === "true"; + +// ==================== 全局状态 ==================== +let audioFiles = []; +let playlist = []; +let heatMap = new Map(); +let metadataMap = new Map(); +let cacheStatus = new Map(); // 'ready' | 'pending' | 'error' +let server = null; +let cacheTotalSize = 0; // 增量追踪 + +let liveSegments = []; // { duration, path, songId } +let mediaSequence = 0; +let currentPlaylistIdx = 0; +let currentSongSegmentIdx = 0; +let songSegmentsCache = new Map(); +let liveTimer = null; + +let retryCount = new Map(); +let heatSaveTimer = null; +let scanDebounceTimer = null; +const sseClients = new Set(); + +// ==================== FFmpeg 并发队列 ==================== +class TaskQueue { + constructor(max) { + this.max = max; + this.running = 0; + this.queue = []; + } + push(fn) { + this.queue.push(fn); + this._drain(); + } + _drain() { + while (this.running < this.max && this.queue.length > 0) { + this.running++; + const task = this.queue.shift(); + task() + .catch((e) => console.error("[QUEUE]", e.message)) + .finally(() => { + this.running--; + this._drain(); + }); + } + } +} +const ffmpegQueue = new TaskQueue(MAX_FFMPEG_CONCURRENT); + +// ==================== 工具函数 ==================== +function fileHash(fp) { + return crypto.createHash("md5").update(fp).digest("hex").slice(0, 16); +} +function getCacheDir(fp) { + return path.join(HLS_CACHE_DIR, fileHash(fp)); +} +function naturalCompare(a, b) { + return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }); +} +function getDirSize(dirPath) { + let size = 0; + if (!fs.existsSync(dirPath)) return size; + for (const entry of fs.readdirSync(dirPath)) { + const full = path.join(dirPath, entry); + try { + const st = fs.statSync(full); + size += st.isDirectory() ? getDirSize(full) : st.size; + } catch (_) {} + } + return size; +} +function clampPlaylistIndex() { + if (playlist.length === 0) { + currentPlaylistIdx = 0; + currentSongSegmentIdx = 0; + return; + } + if (currentPlaylistIdx < 0) currentPlaylistIdx = 0; + if (currentPlaylistIdx >= playlist.length) { + currentPlaylistIdx = 0; + currentSongSegmentIdx = 0; + } +} + +// [FIX] 原子写入:先写临时文件再 rename,播放器不会读到半截 M3U8 +function atomicWriteSync(filePath, data) { + const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`; + fs.writeFileSync(tmp, data, "utf8"); + fs.renameSync(tmp, filePath); +} + +// ==================== 网易云音源导入 ==================== +const NETEASE_UA = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36"; +const NETEASE_REFERER = "http://music.163.com/"; + +function neteaseRequest(url, redirects = 0) { + return new Promise((resolve, reject) => { + const lib = url.startsWith("https:") ? https : http; + const req = lib.get( + url, + { headers: { "User-Agent": NETEASE_UA, Referer: NETEASE_REFERER } }, + (res) => { + const code = res.statusCode; + if ([301, 302, 303, 307, 308].includes(code) && res.headers.location) { + res.resume(); + const loc = res.headers.location; + if (loc.includes("/404")) + return reject(new Error("该歌曲无版权或需 VIP,无法下载")); + if (redirects >= 5) return reject(new Error("重定向次数过多")); + const next = new URL(loc, url).toString(); + return resolve(neteaseRequest(next, redirects + 1)); + } + if (code !== 200) { + res.resume(); + return reject(new Error(`网易云 HTTP ${code}`)); + } + resolve(res); + }, + ); + req.on("error", reject); + req.setTimeout(15000, () => req.destroy(new Error("网易云请求超时"))); + }); +} + +function readResponseBody(res) { + return new Promise((resolve, reject) => { + let body = ""; + res.setEncoding("utf8"); + res.on("data", (d) => (body += d)); + res.on("end", () => resolve(body)); + res.on("error", reject); + }); +} + +function sanitizeFileName(name) { + return name.replace(/[\\/:*?"<>|]/g, "_").trim(); +} + +async function fetchNetEaseDetail(id) { + const url = `http://music.163.com/api/song/detail/?id=${id}&ids=[${id}]`; + const res = await neteaseRequest(url); + const body = await readResponseBody(res); + const json = JSON.parse(body.replace(/^/, "")); + const song = json.songs && json.songs[0]; + if (!song) throw new Error("未找到歌曲(可能是无版权/灰色歌曲)"); + return { + name: song.name, + artist: (song.artists && song.artists[0] && song.artists[0].name) || "", + albumImage: (song.album && song.album.blurPicUrl) || "", + }; +} + +function downloadNetEase(id, filename) { + return new Promise((resolve, reject) => { + const url = `http://music.163.com/song/media/outer/url?id=${id}.mp3`; + neteaseRequest(url) + .then((res) => { + const filePath = path.join(AUDIO_DIR, filename + ".mp3"); + const ws = fs.createWriteStream(filePath); + ws.on("finish", () => { + const ctype = res.headers["content-type"] || ""; + if (!ctype.startsWith("audio")) { + fs.unlinkSync(filePath); + return reject( + new Error(`返回非音频内容 (${ctype}),歌曲可能无版权`), + ); + } + resolve(filePath); + }); + ws.on("error", reject); + res.on("error", reject); + res.pipe(ws); + }) + .catch(reject); + }); +} + +// 网易云音源:歌名搜索(返回精简列表,过滤 fee=1) +async function searchNetEase(keyword, offset = 0, limit = 20) { + const url = + "http://music.163.com/api/search/get/web?csrf_token=hlpretag=&hlposttag=" + + "&s=" + + encodeURIComponent(keyword) + + "&type=1&offset=" + + offset + + "&total=true&limit=" + + limit; + const res = await neteaseRequest(url); + const body = await readResponseBody(res); + const json = JSON.parse(body.replace(/^/, "")); + const songs = (json.result && json.result.songs) || []; + const list = songs + .filter((s) => Number(s.fee) !== 1) + .map((s) => { + const item = { + id: s.id, + name: s.name, + artists: (s.artists || []).map((a) => a.name).filter(Boolean), + duration: s.duration, + }; + const alias = s.alias || []; + if (alias.length) item.alias = alias; + return item; + }); + return { total: list.length, list }; +} + +// ==================== SSE 推送 ==================== +function broadcastSSE(event, data) { + const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; + for (const c of sseClients) { + try { + c.write(msg); + } catch (_) { + sseClients.delete(c); + } + } +} +function getPlaylistSnapshot() { + return playlist.map((file, idx) => ({ + name: path.basename(file), + path: file, + heat: heatMap.get(file) || 0, + cacheStatus: cacheStatus.get(file) || "unknown", + isCurrent: idx === currentPlaylistIdx, + })); +} + +// ==================== 热度管理(防抖落盘)==================== +function loadHeatMap() { + try { + heatMap = new Map( + Object.entries(JSON.parse(fs.readFileSync(HEAT_FILE, "utf8"))), + ); + } catch (_) { + heatMap = new Map(); + } +} +function saveHeatMap() { + try { + fs.writeFileSync( + HEAT_FILE, + JSON.stringify(Object.fromEntries(heatMap), null, 2), + ); + } catch (_) {} +} +function scheduleHeatSave() { + if (heatSaveTimer) clearTimeout(heatSaveTimer); + heatSaveTimer = setTimeout(saveHeatMap, 5000); +} +function incrementHeat(fp) { + heatMap.set(fp, (heatMap.get(fp) || 0) + 1); + scheduleHeatSave(); +} + +// ==================== 播放列表 ==================== +function loadPlaylist() { + try { + playlist = JSON.parse(fs.readFileSync(PLAYLIST_FILE, "utf8")).filter((p) => + fs.existsSync(p), + ); + } catch (_) { + playlist = []; + } +} +function savePlaylist() { + try { + fs.writeFileSync(PLAYLIST_FILE, JSON.stringify(playlist, null, 2)); + } catch (_) {} +} + +// ==================== 切片元数据解析 ==================== +function loadSongSegments(fp) { + if (songSegmentsCache.has(fp)) return songSegmentsCache.get(fp); + const m3u8 = path.join(getCacheDir(fp), "index.m3u8"); + if (!fs.existsSync(m3u8)) return null; + try { + const lines = fs + .readFileSync(m3u8, "utf8") + .split("\n") + .map((l) => l.trim()) + .filter(Boolean); + const segments = []; + let dur = HLS_TIME; + for (const line of lines) { + if (line.startsWith("#EXTINF:")) { + dur = parseFloat(line.split(":")[1].replace(",", "")); + } else if (!line.startsWith("#") && line.endsWith(".ts")) { + segments.push({ duration: dur, fileName: path.basename(line) }); + } + } + if (segments.length === 0) return null; + songSegmentsCache.set(fp, segments); + return segments; + } catch (e) { + console.error("[SEG]", path.basename(fp), e.message); + return null; + } +} + +// ==================== 音频探测 ==================== +function probeAudio(fp) { + return new Promise((resolve, reject) => { + const proc = spawn("ffprobe", [ + "-v", + "error", + "-select_streams", + "a:0", + "-show_entries", + "stream=codec_name,sample_rate,channels:format=duration", + "-of", + "json", + fp, + ]); + let out = "", + err = ""; + proc.stdout.on("data", (d) => (out += d)); + proc.stderr.on("data", (d) => (err += d)); + proc.on("close", (code) => { + if (code !== 0) return reject(new Error(err)); + try { + const data = JSON.parse(out); + const s = data.streams?.[0]; + if (!s) return reject(new Error("无音频流")); + const dur = + data.format && data.format.duration + ? parseFloat(data.format.duration) + : null; + resolve({ + codec: s.codec_name, + sampleRate: parseInt(s.sample_rate, 10), + channels: s.channels, + duration: dur, + }); + } catch (e) { + reject(e); + } + }); + proc.on("error", reject); + }); +} + +// ==================== 音频元数据管理 ==================== +function loadMetadata() { + try { + metadataMap = new Map( + Object.entries(JSON.parse(fs.readFileSync(METADATA_FILE, "utf8"))), + ); + } catch (_) { + metadataMap = new Map(); + } +} +function saveMetadata() { + try { + fs.writeFileSync( + METADATA_FILE, + JSON.stringify(Object.fromEntries(metadataMap), null, 2), + ); + } catch (_) {} +} +// 从「作者 - 歌名.mp3」文件名解析出作者与歌名 +function parseFileName(fileName) { + const stem = fileName.replace(/\.[^.]+$/, ""); + const idx = stem.indexOf(" - "); + if (idx > 0) + return { + artist: stem.slice(0, idx).trim(), + name: stem.slice(idx + 3).trim(), + }; + return { artist: "", name: stem.trim() }; +} +// 补齐某音频的元数据(歌名/作者/封面/时长);已有则跳过,仅补缺失的时长 +async function ensureMetadata(filePath) { + if (metadataMap.has(filePath)) { + const m = metadataMap.get(filePath); + if (m.duration == null) { + try { + m.duration = (await probeAudio(filePath)).duration; + saveMetadata(); + } catch (_) {} + } + return m; + } + const parsed = parseFileName(path.basename(filePath)); + const meta = { + name: parsed.name, + artist: parsed.artist, + albumImage: "", + duration: null, + }; + try { + meta.duration = (await probeAudio(filePath)).duration; + } catch (_) {} + metadataMap.set(filePath, meta); + saveMetadata(); + return meta; +} + +// ==================== [FIX] 启动缓存扫描 ==================== +// 在 preGenerateAll 之前调用,识别已有缓存,避免重复生成 +function scanExistingCaches() { + const hashMap = new Map(); + for (const file of audioFiles) hashMap.set(fileHash(file), file); + + let validCount = 0; + for (const entry of fs.readdirSync(HLS_CACHE_DIR)) { + const dir = path.join(HLS_CACHE_DIR, entry); + try { + if (!fs.statSync(dir).isDirectory()) continue; + } catch (_) { + continue; + } + + const originalFile = hashMap.get(entry); + if (!originalFile) continue; // 孤立缓存,后续由 enforceCacheSizeLimit 清理 + + // 检查 index.m3u8 是否存在且可解析 + const segments = loadSongSegments(originalFile); + if (segments && segments.length > 0) { + // 检查所有 .ts 文件是否完整存在且非空 + const allValid = segments.every((seg) => { + const segPath = path.join(dir, seg.fileName); + try { + return fs.existsSync(segPath) && fs.statSync(segPath).size > 0; + } catch (_) { + return false; + } + }); + + if (allValid) { + cacheStatus.set(originalFile, "ready"); + cacheTotalSize += getDirSize(dir); + validCount++; + } else { + console.log( + `[CACHE] 不完整,将重新生成: ${path.basename(originalFile)}`, + ); + const oldSize = getDirSize(dir); + fs.rmSync(dir, { recursive: true, force: true }); + cacheTotalSize = Math.max(0, cacheTotalSize - oldSize); + songSegmentsCache.delete(originalFile); + } + } else { + // index.m3u8 不存在或无法解析 + const oldSize = getDirSize(dir); + fs.rmSync(dir, { recursive: true, force: true }); + cacheTotalSize = Math.max(0, cacheTotalSize - oldSize); + } + } + console.log( + `[CACHE] 启动扫描: ${validCount}/${audioFiles.length} 个缓存有效,` + + `总大小 ${(cacheTotalSize / 1024 / 1024).toFixed(1)}MB`, + ); +} + +// ==================== 缓存清理 ==================== +function enforceCacheSizeLimit() { + if (cacheTotalSize <= MAX_CACHE_SIZE) return; + console.log( + `[CACHE] ${(cacheTotalSize / 1024 / 1024).toFixed(1)}MB 超限,开始清理`, + ); + + // 保护当前 + 接下来 2 首 + const protectedFiles = new Set(); + for (let i = 0; i < Math.min(3, playlist.length); i++) { + const idx = (currentPlaylistIdx + i) % playlist.length; + if (playlist[idx]) protectedFiles.add(playlist[idx]); + } + + // 保护直播滑动窗口中的歌曲(即使已从播放列表移除) + const liveHashes = new Set(liveSegments.map((s) => s.songId)); + + const items = []; + for (const entry of fs.readdirSync(HLS_CACHE_DIR)) { + const dir = path.join(HLS_CACHE_DIR, entry); + try { + if (!fs.statSync(dir).isDirectory()) continue; + } catch (_) { + continue; + } + let origFile = null; + for (const a of audioFiles) { + if (fileHash(a) === entry) { + origFile = a; + break; + } + } + const size = getDirSize(dir); + items.push({ + dir, + size, + origFile, + inLiveWindow: liveHashes.has(entry), + isProtected: origFile ? protectedFiles.has(origFile) : false, + inPlaylist: origFile ? playlist.includes(origFile) : false, + heat: origFile ? heatMap.get(origFile) || 0 : 0, + }); + } + + // 清理优先级:不受保护 → 不在播放列表 → 不在直播窗口 → 热度低 + items.sort((a, b) => { + if (a.inLiveWindow !== b.inLiveWindow) return a.inLiveWindow ? 1 : -1; + if (a.isProtected !== b.isProtected) return a.isProtected ? 1 : -1; + if (a.inPlaylist !== b.inPlaylist) return a.inPlaylist ? 1 : -1; + return a.heat - b.heat; + }); + + for (const item of items) { + if (cacheTotalSize <= MAX_CACHE_SIZE) break; + console.log( + `[CACHE] 清理: ${path.basename(item.dir)} (${(item.size / 1024 / 1024).toFixed(1)}MB)`, + ); + fs.rmSync(item.dir, { recursive: true, force: true }); + cacheTotalSize = Math.max(0, cacheTotalSize - item.size); + if (item.origFile) { + cacheStatus.set(item.origFile, "pending"); + songSegmentsCache.delete(item.origFile); + if (!item.isProtected) triggerPreGenerate(item.origFile); + } + } +} + +// ==================== HLS 预生成 ==================== +function triggerPreGenerate(filePath) { + const status = cacheStatus.get(filePath); + if (status === "pending" || status === "ready") return; + + const retries = retryCount.get(filePath) || 0; + if (retries >= MAX_RETRIES) { + console.warn( + `[PRESEG] 跳过(已重试 ${MAX_RETRIES} 次): ${path.basename(filePath)}`, + ); + return; + } + retryCount.set(filePath, retries + 1); + cacheStatus.set(filePath, "pending"); + + const cacheDir = getCacheDir(filePath); + // [FIX] 仅清理残留缓存(正常路径不会走到这里,因为 scanExistingCaches 已处理) + if (fs.existsSync(cacheDir)) { + cacheTotalSize = Math.max(0, cacheTotalSize - getDirSize(cacheDir)); + fs.rmSync(cacheDir, { recursive: true, force: true }); + } + fs.mkdirSync(cacheDir, { recursive: true }); + + ffmpegQueue.push(async () => { + // 探测源格式:AAC 44.1kHz 立体声 → copy 模式避免二次压缩损耗 + let audioArgs; + try { + const probe = await probeAudio(filePath); + if ( + probe.codec === "aac" && + probe.sampleRate === 44100 && + probe.channels <= 2 + ) { + audioArgs = ["-c:a", "copy"]; + console.log(`[PRESEG] copy: ${path.basename(filePath)}`); + } else { + audioArgs = [ + "-c:a", + "aac", + "-b:a", + "192k", + "-ar", + "44100", + "-ac", + "2", + "-profile:a", + "aac_low", + ]; + console.log( + `[PRESEG] 转码: ${path.basename(filePath)} (${probe.codec} ${probe.sampleRate}Hz)`, + ); + } + } catch (_) { + audioArgs = [ + "-c:a", + "aac", + "-b:a", + "192k", + "-ar", + "44100", + "-ac", + "2", + "-profile:a", + "aac_low", + ]; + } + + return new Promise((resolve, reject) => { + const outputM3u8 = path.join(cacheDir, "index.m3u8"); + const args = [ + "-y", + "-fflags", + "+genpts+discardcorrupt", + "-i", + filePath, + ...audioArgs, + "-vn", + "-avoid_negative_ts", + "make_zero", + "-hls_time", + String(HLS_TIME), + "-hls_list_size", + "0", + "-hls_segment_type", + "mpegts", + "-hls_flags", + "independent_segments", + "-max_muxing_queue_size", + "2048", + "-f", + "hls", + outputM3u8, + ]; + + const proc = spawn("ffmpeg", args); + let stderrBuf = ""; + proc.stderr.on("data", (d) => { + stderrBuf += d.toString(); + if (stderrBuf.length > 4096) stderrBuf = stderrBuf.slice(-4096); + }); + + // [FIX] 超时保护:防止 FFmpeg 挂死阻塞队列 + const timer = setTimeout(() => { + proc.kill("SIGKILL"); + console.error(`[PRESEG] 超时终止: ${path.basename(filePath)}`); + }, FFMPEG_TIMEOUT); + + proc.on("close", (code) => { + clearTimeout(timer); + if (code === 0) { + console.log(`[PRESEG] 完成: ${path.basename(filePath)}`); + cacheStatus.set(filePath, "ready"); + songSegmentsCache.delete(filePath); + retryCount.delete(filePath); + cacheTotalSize += getDirSize(cacheDir); + enforceCacheSizeLimit(); + broadcastSSE("cache-status", { + file: path.basename(filePath), + status: "ready", + }); + resolve(); + } else { + cacheStatus.set(filePath, "error"); + if (fs.existsSync(cacheDir)) + fs.rmSync(cacheDir, { recursive: true, force: true }); + console.error( + `[PRESEG] 失败 code=${code}: ${path.basename(filePath)}`, + ); + reject(new Error(`FFmpeg exit ${code}`)); + } + }); + + proc.on("error", (err) => { + clearTimeout(timer); + cacheStatus.set(filePath, "error"); + reject(err); + }); + }); + }); +} + +function preGenerateAll() { + for (const file of audioFiles) { + const status = cacheStatus.get(file); + if (!status || status === "error") triggerPreGenerate(file); + } +} + +// ==================== 直播流核心 ==================== +function generateLivePlaylist() { + if (liveSegments.length === 0) { + atomicWriteSync( + path.join(HLS_OUTPUT_DIR, "index.m3u8"), + `#EXTM3U\n#EXT-X-VERSION:3\n` + + `#EXT-X-TARGETDURATION:${HLS_TIME + 1}\n` + + `#EXT-X-INDEPENDENT-SEGMENTS\n` + + `#EXT-X-MEDIA-SEQUENCE:0\n`, + ); + return; + } + + // TARGETDURATION = 所有切片最大时长向上取整 + 1(安全余量) + const maxDur = liveSegments.reduce( + (m, s) => Math.max(m, s.duration), + HLS_TIME, + ); + const targetDur = Math.max(HLS_TIME + 1, Math.ceil(maxDur) + 1); + + let content = "#EXTM3U\n#EXT-X-VERSION:3\n"; + content += `#EXT-X-TARGETDURATION:${targetDur}\n`; + content += "#EXT-X-INDEPENDENT-SEGMENTS\n"; // 每段可独立解码 + content += `#EXT-X-MEDIA-SEQUENCE:${mediaSequence}\n`; + + for (let i = 0; i < liveSegments.length; i++) { + // 可选:仅在启用时插入不连续性标记 + if ( + USE_DISCONTINUITY && + i > 0 && + liveSegments[i].songId !== liveSegments[i - 1].songId + ) { + content += "#EXT-X-DISCONTINUITY\n"; + } + content += `#EXTINF:${liveSegments[i].duration.toFixed(3)},\n${liveSegments[i].path}\n`; + } + + // [FIX] 原子写入 + atomicWriteSync(path.join(HLS_OUTPUT_DIR, "index.m3u8"), content); +} + +function advanceLiveStream() { + if (audioFiles.length === 0) return; + + if (playlist.length === 0) { + playlist.push(audioFiles[Math.floor(Math.random() * audioFiles.length)]); + savePlaylist(); + currentPlaylistIdx = 0; + currentSongSegmentIdx = 0; + broadcastSSE("playlist-update", getPlaylistSnapshot()); + } + + let safety = playlist.length + 2; + while (safety-- > 0) { + clampPlaylistIndex(); + + const currentSong = playlist[currentPlaylistIdx]; + const status = cacheStatus.get(currentSong); + + // 未就绪 → 跳下一首 + if (status !== "ready") { + if (status !== "pending") triggerPreGenerate(currentSong); + currentPlaylistIdx = (currentPlaylistIdx + 1) % playlist.length; + currentSongSegmentIdx = 0; + continue; + } + + const segments = loadSongSegments(currentSong); + if (!segments || segments.length === 0) { + currentPlaylistIdx = (currentPlaylistIdx + 1) % playlist.length; + currentSongSegmentIdx = 0; + continue; + } + + // ===== 歌曲播放完毕 → 从列表中移除 ===== + if (currentSongSegmentIdx >= segments.length) { + const finishedName = path.basename(currentSong); + // splice 只移除当前位置的一个实例,同歌其他实例保留 + playlist.splice(currentPlaylistIdx, 1); + songSegmentsCache.delete(currentSong); + currentSongSegmentIdx = 0; + + // 列表空了 → 自动补一首 + if (playlist.length === 0 && audioFiles.length > 0) { + playlist.push( + audioFiles[Math.floor(Math.random() * audioFiles.length)], + ); + currentPlaylistIdx = 0; + } + + savePlaylist(); + clampPlaylistIndex(); + broadcastSSE("song-finished", { name: finishedName }); + broadcastSSE("playlist-update", getPlaylistSnapshot()); + + // 确保下一首缓存就绪 + if ( + playlist.length > 0 && + cacheStatus.get(playlist[currentPlaylistIdx]) !== "ready" + ) { + triggerPreGenerate(playlist[currentPlaylistIdx]); + } + + continue; // 立即尝试追加下一首的切片,不留间隙 + } + + // ===== 正常追加切片 ===== + const seg = segments[currentSongSegmentIdx]; + const songId = fileHash(currentSong); + liveSegments.push({ + duration: seg.duration, + path: path.posix.join("/hls_cache", songId, seg.fileName), + songId, + }); + + while (liveSegments.length > SLIDE_WINDOW_SIZE) { + liveSegments.shift(); + mediaSequence++; + } + + currentSongSegmentIdx++; + generateLivePlaylist(); + return; // 成功追加一个切片,等下一个 tick + } + + console.warn("[LIVE] 所有歌曲均不可用"); +} + +function startLiveStream() { + if (liveTimer) clearInterval(liveTimer); + + // 预填充窗口(以实际长度为终止条件) + let attempts = 0; + const target = Math.floor(SLIDE_WINDOW_SIZE / 2); + while (liveSegments.length < target && attempts < SLIDE_WINDOW_SIZE * 2) { + const prev = liveSegments.length; + advanceLiveStream(); + attempts++; + if (liveSegments.length === prev && liveSegments.length === 0) break; + } + + liveTimer = setInterval(advanceLiveStream, LIVE_TICK_INTERVAL); + console.log( + `[LIVE] 已启动 | 切片 ${HLS_TIME}s | 窗口 ${SLIDE_WINDOW_SIZE} 段 ≈${SLIDE_WINDOW_SIZE * HLS_TIME}s | 预填充 ${liveSegments.length} 段`, + ); +} + +// ==================== 资源扫描 ==================== +function scanAudioFiles() { + const exts = [".mp3", ".m4a", ".flac", ".wav"]; + audioFiles = fs + .readdirSync(AUDIO_DIR) + .filter((n) => exts.includes(path.extname(n).toLowerCase())) + .map((n) => path.join(AUDIO_DIR, n)) + .sort(naturalCompare); + console.log(`[INFO] 资源: ${audioFiles.length} 个文件`); +} + +function sanitizePlaylist() { + const valid = new Set(audioFiles); + for (const p of playlist) { + if (!valid.has(p)) songSegmentsCache.delete(p); + } + playlist = playlist.filter((p) => valid.has(p)); + clampPlaylistIndex(); + savePlaylist(); +} + +// ==================== [FIX] 初始化顺序 ==================== +// 1. 加载持久化数据 +// 2. 扫描文件列表 +// 3. 验证已有缓存(关键!在 preGenerateAll 之前) +// 4. 只生成缺失的缓存 +loadHeatMap(); +loadMetadata(); +loadPlaylist(); +scanAudioFiles(); +audioFiles.forEach((f) => ensureMetadata(f)); // 异步补全元数据(封面/时长等) +scanExistingCaches(); // ← 识别已有缓存,设为 ready +preGenerateAll(); // ← 只处理 status 为空或 error 的文件 +enforceCacheSizeLimit(); +sanitizePlaylist(); +startLiveStream(); + +// ==================== 文件监听(防抖)==================== +const watcher = chokidar.watch(AUDIO_DIR, { + ignoreInitial: true, + awaitWriteFinish: { stabilityThreshold: 500 }, +}); +function scheduleScan() { + if (scanDebounceTimer) clearTimeout(scanDebounceTimer); + scanDebounceTimer = setTimeout(() => { + scanAudioFiles(); + audioFiles.forEach((f) => ensureMetadata(f)); + preGenerateAll(); + enforceCacheSizeLimit(); + sanitizePlaylist(); + }, 500); +} +watcher.on("add", scheduleScan); +watcher.on("unlink", scheduleScan); + +// ==================== 认证 ==================== +function authMiddleware(req, res, next) { + if (!AUTH_TOKEN) return next(); + if (req.headers.authorization === `Bearer ${AUTH_TOKEN}`) return next(); + return res.status(401).json({ error: "未授权" }); +} + +// ==================== HTTP API ==================== +const app = express(); +app.use((req, res, next) => { + res.header("Access-Control-Allow-Origin", "*"); + res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + res.header("Access-Control-Allow-Headers", "Content-Type, Authorization"); + if (req.method === "OPTIONS") return res.sendStatus(200); + next(); +}); +app.use(express.json()); +app.use(express.static(PUBLIC_DIR)); + +// [FIX] M3U8 禁止浏览器缓存,确保每次都获取最新版本 +app.use( + "/hls", + express.static(HLS_OUTPUT_DIR, { + setHeaders: (res, filePath) => { + if (filePath.endsWith(".m3u8")) { + res.set("Content-Type", "application/vnd.apple.mpegurl; charset=utf-8"); + res.set("Cache-Control", "no-cache, no-store, must-revalidate"); + } + }, + }), +); +app.use("/hls_cache", express.static(HLS_CACHE_DIR)); + +app.get("/api/events", (req, res) => { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }); + res.write(":ok\n\n"); + sseClients.add(res); + req.on("close", () => sseClients.delete(res)); +}); + +app.use("/api", authMiddleware); + +function isValidFileName(f) { + return ( + typeof f === "string" && + f.length > 0 && + f.length < 500 && + !f.includes("..") && + !f.includes("/") && + !f.includes("\\") && + !f.includes("\0") + ); +} + +app.get("/api/resources", (req, res) => { + res.json( + audioFiles.map((f) => { + const m = metadataMap.get(f) || { + name: path.basename(f), + artist: "", + albumImage: "", + duration: null, + }; + let fileSize = 0; + try { + fileSize = fs.statSync(f).size; + } catch (_) {} + return { + file: path.basename(f), + path: f, + title: m.name, + artist: m.artist || "", + albumImage: m.albumImage || "", + duration: m.duration || null, + fileSize, + heat: heatMap.get(f) || 0, + cacheStatus: cacheStatus.get(f) || "unknown", + cacheSize: (() => { + const d = getCacheDir(f); + return fs.existsSync(d) ? getDirSize(d) : 0; + })(), + }; + }), + ); +}); + +// 网易云音源:歌名搜索 +app.get("/api/music/search", async (req, res) => { + const { keyword } = req.query; + if (!keyword || !String(keyword).trim()) + return res.status(400).json({ error: "缺少搜索关键词" }); + try { + res.json(await searchNetEase(String(keyword).trim())); + } catch (e) { + res.status(400).json({ error: e.message }); + } +}); + +// 网易云音源:查询歌曲详情(歌名/作者/专辑图) +app.post("/api/music/detail", async (req, res) => { + const { id } = req.body; + if (!id || !/^\d+$/.test(String(id))) + return res.status(400).json({ error: "无效 id" }); + try { + res.json(await fetchNetEaseDetail(String(id))); + } catch (e) { + res.status(400).json({ error: e.message }); + } +}); + +// 网易云音源:下载入库(保存到 audio/,watcher 自动切片) +app.post("/api/music/download", async (req, res) => { + const { id, name, artist, albumImage } = req.body; + if (!id || !/^\d+$/.test(String(id))) + return res.status(400).json({ error: "无效 id" }); + try { + // 缺省时重新查详情,统一拿到歌名/作者/封面 + let meta = { name, artist: artist || "", albumImage: albumImage || "" }; + if (!meta.name) { + const info = await fetchNetEaseDetail(String(id)); + meta = { + name: info.name, + artist: info.artist || "", + albumImage: info.albumImage || "", + }; + } + const filename = sanitizeFileName( + meta.artist ? `${meta.artist} - ${meta.name}` : meta.name, + ); + if (!filename) return res.status(400).json({ error: "缺少歌名" }); + + // 防重复:同名文件已存在则不覆盖(覆盖触发 change 而非 add,不会重新切片) + if (fs.existsSync(path.join(AUDIO_DIR, filename + ".mp3"))) + return res.status(409).json({ error: `已存在: ${filename}.mp3` }); + + const filePath = await downloadNetEase(String(id), filename); + metadataMap.set(filePath, { ...meta, duration: null }); // 时长稍后由 ensureMetadata 补齐 + saveMetadata(); + res.json({ success: true, file: path.basename(filePath) }); + } catch (e) { + res.status(400).json({ error: e.message }); + } +}); + +app.get("/api/playlist", (req, res) => res.json(getPlaylistSnapshot())); + +app.post("/api/playlist/add", (req, res) => { + const { file } = req.body; + if (!isValidFileName(file)) + return res.status(400).json({ error: "无效文件名" }); + const tp = path.join(AUDIO_DIR, file); + if (!audioFiles.includes(tp)) + return res.status(404).json({ error: "资源不存在" }); + playlist.push(tp); + incrementHeat(tp); + savePlaylist(); + if (cacheStatus.get(tp) !== "ready") triggerPreGenerate(tp); + broadcastSSE("playlist-update", getPlaylistSnapshot()); + res.json({ success: true, playlist: playlist.map((p) => path.basename(p)) }); +}); + +app.post("/api/playlist/remove", (req, res) => { + const { index } = req.body; + if ( + typeof index !== "number" || + !Number.isInteger(index) || + index < 0 || + index >= playlist.length + ) + return res.status(400).json({ error: "无效索引" }); + if (index < currentPlaylistIdx) currentPlaylistIdx--; + else if (index === currentPlaylistIdx) currentSongSegmentIdx = 0; + playlist.splice(index, 1); + if (playlist.length === 0 && audioFiles.length > 0) { + playlist.push(audioFiles[Math.floor(Math.random() * audioFiles.length)]); + currentPlaylistIdx = 0; + currentSongSegmentIdx = 0; + } + clampPlaylistIndex(); + savePlaylist(); + broadcastSSE("playlist-update", getPlaylistSnapshot()); + res.json({ success: true, playlist: playlist.map((p) => path.basename(p)) }); +}); + +app.post("/api/playlist/reorder", (req, res) => { + const { newOrder } = req.body; + if (!Array.isArray(newOrder)) + return res.status(400).json({ error: "无效数据" }); + const valid = new Set(audioFiles); + const cur = playlist[currentPlaylistIdx]; + playlist = newOrder.filter((p) => typeof p === "string" && valid.has(p)); + const ni = playlist.indexOf(cur); + currentPlaylistIdx = ni !== -1 ? ni : 0; + if (ni === -1) currentSongSegmentIdx = 0; + if (playlist.length === 0 && audioFiles.length > 0) { + playlist.push(audioFiles[Math.floor(Math.random() * audioFiles.length)]); + currentPlaylistIdx = 0; + currentSongSegmentIdx = 0; + } + savePlaylist(); + broadcastSSE("playlist-update", getPlaylistSnapshot()); + res.json({ success: true, playlist: playlist.map((p) => path.basename(p)) }); +}); + +app.post("/api/playlist/clear", (req, res) => { + for (const f of playlist) songSegmentsCache.delete(f); + playlist = []; + if (audioFiles.length > 0) + playlist.push(audioFiles[Math.floor(Math.random() * audioFiles.length)]); + currentPlaylistIdx = 0; + currentSongSegmentIdx = 0; + savePlaylist(); + broadcastSSE("playlist-update", getPlaylistSnapshot()); + res.json({ success: true, playlist: playlist.map((p) => path.basename(p)) }); +}); + +app.post("/api/playlist/next", (req, res) => { + if (playlist.length === 0) + return res.status(400).json({ error: "播放列表为空" }); + currentPlaylistIdx = (currentPlaylistIdx + 1) % playlist.length; + currentSongSegmentIdx = 0; + if (cacheStatus.get(playlist[currentPlaylistIdx]) !== "ready") + triggerPreGenerate(playlist[currentPlaylistIdx]); + broadcastSSE("now-playing", { + index: currentPlaylistIdx, + name: path.basename(playlist[currentPlaylistIdx]), + }); + broadcastSSE("playlist-update", getPlaylistSnapshot()); + res.json({ + success: true, + current: path.basename(playlist[currentPlaylistIdx]), + }); +}); + +app.post("/api/playlist/prev", (req, res) => { + if (playlist.length === 0) + return res.status(400).json({ error: "播放列表为空" }); + currentPlaylistIdx = + (currentPlaylistIdx - 1 + playlist.length) % playlist.length; + currentSongSegmentIdx = 0; + if (cacheStatus.get(playlist[currentPlaylistIdx]) !== "ready") + triggerPreGenerate(playlist[currentPlaylistIdx]); + broadcastSSE("now-playing", { + index: currentPlaylistIdx, + name: path.basename(playlist[currentPlaylistIdx]), + }); + broadcastSSE("playlist-update", getPlaylistSnapshot()); + res.json({ + success: true, + current: path.basename(playlist[currentPlaylistIdx]), + }); +}); + +app.post("/api/playlist/jump", (req, res) => { + const { index } = req.body; + if ( + typeof index !== "number" || + !Number.isInteger(index) || + index < 0 || + index >= playlist.length + ) + return res.status(400).json({ error: "无效索引" }); + currentPlaylistIdx = index; + currentSongSegmentIdx = 0; + if (cacheStatus.get(playlist[currentPlaylistIdx]) !== "ready") + triggerPreGenerate(playlist[currentPlaylistIdx]); + broadcastSSE("now-playing", { + index: currentPlaylistIdx, + name: path.basename(playlist[currentPlaylistIdx]), + }); + broadcastSSE("playlist-update", getPlaylistSnapshot()); + res.json({ + success: true, + current: path.basename(playlist[currentPlaylistIdx]), + }); +}); + +app.get("/api/cache/size", (req, res) => { + res.json({ + total: cacheTotalSize, + limit: MAX_CACHE_SIZE, + usedPercent: ((cacheTotalSize / MAX_CACHE_SIZE) * 100).toFixed(2), + }); +}); + +app.post("/api/cache/cleanup", (req, res) => { + enforceCacheSizeLimit(); + res.json({ success: true, total: cacheTotalSize, limit: MAX_CACHE_SIZE }); +}); + +// ==================== 启动 ==================== +server = app.listen(HTTP_PORT, () => { + console.log(`[HTTP] 面板: http://127.0.0.1:${HTTP_PORT}`); + console.log(`[HTTP] 直播: http://127.0.0.1:${HTTP_PORT}/hls/index.m3u8`); + if (AUTH_TOKEN) console.log("[HTTP] 认证已启用"); +}); + +process.on("uncaughtException", (err) => { + console.error("[FATAL]", err.message, err.stack); +}); +process.on("unhandledRejection", (r) => console.error("[WARN]", r)); + +process.on("SIGINT", () => { + console.log("\n[EXIT] 正在关闭..."); + if (liveTimer) clearInterval(liveTimer); + if (heatSaveTimer) clearTimeout(heatSaveTimer); + if (scanDebounceTimer) clearTimeout(scanDebounceTimer); + for (const c of sseClients) + try { + c.end(); + } catch (_) {} + saveHeatMap(); + watcher.close().then(() => server.close(() => process.exit(0))); +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c20a2af --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1022 @@ +{ + "name": "music-online-demo", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "music-online-demo", + "version": "1.0.0", + "dependencies": { + "chokidar": "^3.6.0", + "express": "^4.21.2" + }, + "engines": { + "node": ">=14.14.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.8", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.8.tgz", + "integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.16.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.3", + "resolved": "https://registry.npmmirror.com/express/-/express-4.22.3.tgz", + "integrity": "sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.13", + "proxy-addr": "~2.0.7", + "qs": "~6.16.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9b32b18 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "music-online-demo", + "version": "1.0.0", + "description": "音乐在线电台:FFmpeg HLS 直播流服务", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "engines": { + "node": ">=14.14.0" + }, + "dependencies": { + "chokidar": "^3.6.0", + "express": "^4.21.2" + } +} diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..ff7ac98 --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,1750 @@ + /* ==================== TOKENS ==================== */ + :root { + --bg: #f4f5f9; + --surface: #fff; + --surface-hover: #fafbff; + --accent: #4f6ef7; + --accent-hover: #3d5ce0; + --accent-light: #eef1fe; + --accent-glow: rgba(79, 110, 247, 0.14); + --accent-subtle: rgba(79, 110, 247, 0.06); + --text-primary: #0f1225; + --text-secondary: #5a607f; + --text-muted: #9499b8; + --border: #e8eaf2; + --border-light: #f0f1f6; + --danger: #ef4565; + --danger-light: #fef0f2; + --success: #30a46c; + --success-light: #e9f9ef; + --warning: #f5a623; + --warning-light: #fff7e8; + --radius-sm: 12px; + --radius: 16px; + --radius-lg: 24px; + --radius-xl: 28px; + --shadow-xs: 0 1px 2px rgba(15, 18, 37, 0.04); + --shadow-sm: 0 2px 8px rgba(15, 18, 37, 0.06); + --shadow: 0 4px 20px rgba(15, 18, 37, 0.07); + --shadow-lg: 0 8px 40px rgba(15, 18, 37, 0.1); + --content-w: 920px; + --nav-bottom: 14px; + --nav-h: 56px; + --mini-h: 0px; + --total-bottom: calc( + var(--nav-bottom) + var(--nav-h) + var(--mini-h) + 14px + ); + } + .mini-player-visible { + --mini-h: 82px; + } + + /* ==================== RESET ==================== */ + *, + *::before, + *::after { + box-sizing: border-box; + margin: 0; + padding: 0; + } + body { + font-family: + "Plus Jakarta Sans", + -apple-system, + BlinkMacSystemFont, + sans-serif; + color: var(--text-primary); + height: 100dvh; + display: flex; + flex-direction: column; + -webkit-font-smoothing: antialiased; + overflow: hidden; + } + img { + display: block; + max-width: 100%; + height: auto; + } + button { + font-family: inherit; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + } + input { + font-family: inherit; + } + + /* ==================== BG ==================== */ + .page-bg { + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + background: + radial-gradient( + ellipse at 20% 0%, + rgba(79, 110, 247, 0.04) 0%, + transparent 50% + ), + radial-gradient( + ellipse at 80% 100%, + rgba(139, 92, 246, 0.03) 0%, + transparent 50% + ), + var(--bg); + } + .bg-shapes { + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + overflow: hidden; + } + .bg-shapes .s { + position: absolute; + border-radius: 50%; + opacity: 0.03; + animation: floatShape 22s ease-in-out infinite alternate; + } + .bg-shapes .s:nth-child(1) { + width: 520px; + height: 520px; + top: -15%; + left: -10%; + background: var(--accent); + } + .bg-shapes .s:nth-child(2) { + width: 420px; + height: 420px; + bottom: -10%; + right: -8%; + background: #8b5cf6; + animation-delay: -6s; + } + .bg-shapes .s:nth-child(3) { + width: 300px; + height: 300px; + top: 40%; + left: 60%; + background: var(--success); + animation-delay: -12s; + } + @keyframes floatShape { + 0% { + transform: translate(0, 0) scale(1); + } + 100% { + transform: translate(40px, -30px) scale(1.12); + } + } + + /* ==================== HEADER ==================== */ + .top-header { + flex-shrink: 0; + z-index: 100; + background: rgba(244, 245, 249, 0.78); + backdrop-filter: blur(28px) saturate(180%); + padding: 0.55rem 1rem; + animation: headerSlide 0.5s cubic-bezier(0.22, 1, 0.36, 1); + } + @keyframes headerSlide { + from { + opacity: 0; + transform: translateY(-100%); + } + to { + opacity: 1; + transform: translateY(0); + } + } + .header-inner { + max-width: var(--content-w); + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5rem; + } + .brand { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 800; + font-size: 0.98rem; + color: var(--text-primary); + letter-spacing: -0.02em; + } + .brand-icon { + width: 30px; + height: 30px; + border-radius: 9px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + box-shadow: 0 2px 12px rgba(79, 110, 247, 0.3); + flex-shrink: 0; + } + .brand-icon img { + width: 100%; + height: 100%; + object-fit: cover; + } + .live-pill { + display: inline-flex; + align-items: center; + gap: 0.28rem; + font-size: 0.56rem; + font-weight: 700; + background: var(--success); + color: #fff; + padding: 0.18rem 0.5rem; + border-radius: 20px; + letter-spacing: 0.06em; + text-transform: uppercase; + animation: livePulse 2.5s ease-in-out infinite; + } + .live-dot { + width: 4px; + height: 4px; + background: #fff; + border-radius: 50%; + animation: dotPulse 1.5s ease-in-out infinite; + } + @keyframes livePulse { + 0%, + 100% { + box-shadow: 0 0 0 0 rgba(48, 164, 108, 0.35); + } + 50% { + box-shadow: 0 0 0 8px rgba(48, 164, 108, 0); + } + } + @keyframes dotPulse { + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.3; + transform: scale(0.8); + } + } + .header-actions { + display: flex; + align-items: center; + gap: 0.45rem; + } + .cache-pill { + font-size: 0.66rem; + font-weight: 500; + color: var(--text-secondary); + background: var(--surface); + padding: 0.22rem 0.55rem; + border-radius: 20px; + display: inline-flex; + align-items: center; + gap: 0.28rem; + border: 1px solid var(--border-light); + font-family: "JetBrains Mono", monospace; + letter-spacing: -0.02em; + } + .btn-link-sm { + font-size: 0.7rem; + font-weight: 600; + color: var(--accent); + text-decoration: none; + padding: 0.28rem 0.6rem; + border-radius: 10px; + transition: all 0.2s; + display: inline-flex; + align-items: center; + gap: 0.25rem; + } + .btn-link-sm:hover { + background: var(--accent-light); + } + + /* ==================== PAGE CONTAINER ==================== */ + .page-container { + position: relative; + z-index: 1; + max-width: var(--content-w); + margin: 0 auto; + width: 100%; + padding: 1rem 1.25rem 2rem; + padding-bottom: calc(var(--total-bottom) + 1.5rem); + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + } + .page-container::-webkit-scrollbar { + width: 5px; + } + .page-container::-webkit-scrollbar-track { + background: transparent; + } + .page-container::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 10px; + } + .page-container::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); + } + .page { + display: none; + } + .page.active { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + animation: pageEnter 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; + } + @keyframes pageEnter { + from { + opacity: 0; + transform: translateY(12px) scale(0.995); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } + } + + /* ==================== CARDS ==================== */ + .section-card, + .search-card, + .about-card { + background: var(--surface); + border-radius: var(--radius-xl); + box-shadow: var(--shadow); + border: 1px solid var(--border-light); + animation: cardRise 0.5s cubic-bezier(0.22, 1, 0.36, 1) both; + } + .section-card { + display: flex; + flex-direction: column; + min-height: 0; + } + @keyframes cardRise { + from { + opacity: 0; + transform: translateY(18px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + /* ==================== SEARCH ==================== */ + .search-card { + display: flex; + flex-direction: column; + min-height: 0; + overflow-y: auto; + } + .search-header { + padding: 1.5rem 1.5rem 0.3rem; + } + .search-header h2 { + font-size: 1.12rem; + font-weight: 800; + letter-spacing: -0.03em; + margin-bottom: 0.12rem; + } + .search-header p { + font-size: 0.78rem; + color: var(--text-muted); + } + .search-input-wrap { + padding: 0.75rem 1.5rem; + } + .search-field { + display: flex; + gap: 0.5rem; + align-items: stretch; + } + .search-field input { + flex: 1; + border: 1.5px solid var(--border); + border-radius: var(--radius-sm); + padding: 0.65rem 0.85rem; + font-size: 0.84rem; + background: var(--bg); + color: var(--text-primary); + outline: none; + transition: + border-color 0.25s, + box-shadow 0.25s, + background 0.25s; + } + .search-field input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); + background: var(--surface); + } + .search-field input::placeholder { + color: var(--text-muted); + font-size: 0.82rem; + } + .btn-search { + border: none; + background: var(--accent); + color: #fff; + padding: 0 1.2rem; + border-radius: var(--radius-sm); + font-weight: 700; + font-size: 0.82rem; + transition: all 0.2s; + white-space: nowrap; + letter-spacing: -0.01em; + position: relative; + overflow: hidden; + } + .btn-search::after { + content: ""; + position: absolute; + inset: 0; + background: rgba(255, 255, 255, 0.2); + transform: translateX(-100%); + transition: transform 0.3s; + } + .btn-search:hover::after { + transform: translateX(0); + } + .btn-search:hover { + background: var(--accent-hover); + box-shadow: 0 4px 16px rgba(79, 110, 247, 0.25); + } + .btn-search:active { + transform: scale(0.97); + } + .btn-search:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + /* Parse hint */ + .parse-hint { + padding: 0 1.5rem; + margin-bottom: 0.3rem; + display: flex; + align-items: center; + gap: 0.35rem; + font-size: 0.68rem; + color: var(--text-muted); + max-height: 0; + opacity: 0; + overflow: hidden; + transition: all 0.3s ease; + } + .parse-hint.visible { + opacity: 1; + max-height: 36px; + padding-top: 0.1rem; + } + .parse-hint i { + color: var(--accent); + font-size: 0.75rem; + } + .parse-hint code { + font-family: "JetBrains Mono", monospace; + font-size: 0.64rem; + background: var(--accent-light); + color: var(--accent); + padding: 0.08rem 0.35rem; + border-radius: 5px; + font-weight: 500; + } + + /* Preview card — info only, no inline player */ + .preview-card { + margin: 0 1.5rem 1.5rem; + background: var(--bg); + border-radius: var(--radius-lg); + overflow: hidden; + border: 1px solid var(--border-light); + animation: slideUp 0.4s cubic-bezier(0.22, 1, 0.36, 1); + } + @keyframes slideUp { + from { + opacity: 0; + transform: translateY(16px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + .preview-cover-section { + display: flex; + align-items: center; + gap: 0.9rem; + padding: 0.9rem 1.1rem; + } + .preview-cover-wrap { + position: relative; + width: 64px; + height: 64px; + flex-shrink: 0; + } + .preview-cover { + width: 64px; + height: 64px; + border-radius: var(--radius); + object-fit: cover; + box-shadow: var(--shadow-sm); + } + .preview-cover-wrap::after { + content: ""; + position: absolute; + inset: 0; + border-radius: var(--radius); + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06); + pointer-events: none; + } + .preview-info { + flex: 1; + min-width: 0; + } + .preview-title { + font-weight: 700; + font-size: 0.95rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + letter-spacing: -0.02em; + margin-bottom: 0.1rem; + } + .preview-artist { + font-size: 0.78rem; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .preview-details { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + padding: 0 1.1rem 0.9rem; + } + .detail-tag { + display: inline-flex; + align-items: center; + gap: 0.25rem; + font-size: 0.66rem; + font-weight: 500; + color: var(--text-secondary); + background: var(--surface); + padding: 0.24rem 0.55rem; + border-radius: 10px; + border: 1px solid var(--border-light); + } + .detail-tag i { + font-size: 0.7rem; + color: var(--text-muted); + } + .detail-tag.album-tag i { + color: var(--accent); + } + .detail-tag.quality-tag i { + color: var(--success); + } + .detail-tag.duration-tag i { + color: var(--warning); + } + .preview-actions { + display: flex; + gap: 0.5rem; + padding: 0 1.1rem 1.1rem; + } + .btn-download { + border: none; + background: var(--accent); + color: #fff; + padding: 0.5rem 1rem; + border-radius: var(--radius-sm); + font-weight: 700; + font-size: 0.78rem; + transition: all 0.2s; + display: inline-flex; + align-items: center; + gap: 0.35rem; + white-space: nowrap; + letter-spacing: -0.01em; + flex: 1; + justify-content: center; + position: relative; + overflow: hidden; + } + .btn-download::after { + content: ""; + position: absolute; + inset: 0; + background: rgba(255, 255, 255, 0.2); + transform: translateX(-100%); + transition: transform 0.3s; + } + .btn-download:hover::after { + transform: translateX(0); + } + .btn-download:hover { + background: var(--accent-hover); + box-shadow: 0 4px 16px rgba(79, 110, 247, 0.25); + } + .btn-download:active { + transform: scale(0.97); + } + .btn-download:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + /* Search results */ + .searchResults { + margin: 0 1.5rem 1.5rem; + animation: slideUp 0.4s cubic-bezier(0.22, 1, 0.36, 1); + } + .sr-head { + font-size: 0.74rem; + color: var(--text-muted); + margin-bottom: 0.5rem; + padding: 0 0.25rem; + } + .sr-head b { + color: var(--accent); + font-weight: 700; + } + .sr-list { + display: flex; + flex-direction: column; + gap: 0.3rem; + } + .sr-item { + display: flex; + align-items: center; + gap: 0.65rem; + padding: 0.55rem 0.75rem; + background: var(--bg); + border: 1.5px solid transparent; + border-radius: var(--radius); + cursor: pointer; + transition: all 0.25s ease; + opacity: 0; + animation: srFadeIn 0.35s ease forwards; + } + .sr-item:hover { + border-color: var(--accent); + box-shadow: 0 2px 10px rgba(79, 110, 247, 0.12); + transform: translateY(-1px); + } + .sr-item.active { + border-color: var(--accent); + background: var(--accent-light); + } + @keyframes srFadeIn { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + .sr-main { + flex: 1; + min-width: 0; + } + .sr-name { + font-weight: 600; + font-size: 0.84rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .sr-artist { + font-size: 0.72rem; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 0.06rem; + } + .sr-alias { + font-size: 0.66rem; + color: var(--text-muted); + background: var(--surface); + padding: 0.15rem 0.4rem; + border-radius: 7px; + border: 1px solid var(--border-light); + max-width: 160px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .sr-duration { + font-family: "JetBrains Mono", monospace; + font-size: 0.7rem; + color: var(--text-muted); + flex-shrink: 0; + } + + /* Tips — expand on tall screens */ + .search-tips { + padding: 2rem 1.5rem 2.5rem; + text-align: center; + } + .tips-icon { + width: 60px; + height: 60px; + background: var(--accent-light); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 0.9rem; + font-size: 1.3rem; + color: var(--accent); + animation: float 3s ease-in-out infinite; + } + @keyframes float { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-7px); + } + } + .tips-title { + font-weight: 700; + font-size: 0.9rem; + margin-bottom: 0.3rem; + letter-spacing: -0.02em; + } + .tips-desc { + font-size: 0.76rem; + color: var(--text-muted); + line-height: 1.7; + } + .tips-desc code { + font-family: "JetBrains Mono", monospace; + font-size: 0.68rem; + background: var(--accent-light); + color: var(--accent); + padding: 0.08rem 0.3rem; + border-radius: 5px; + font-weight: 500; + } + + /* ==================== RESOURCE / PLAYLIST ==================== */ + .section-header { + padding: 1.1rem 1.1rem 0.65rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.6rem; + flex-wrap: wrap; + flex-shrink: 0; + } + .section-title { + font-size: 0.92rem; + font-weight: 800; + display: flex; + align-items: center; + gap: 0.4rem; + letter-spacing: -0.03em; + } + .section-count { + min-width: 20px; + height: 20px; + background: var(--accent-light); + color: var(--accent); + font-size: 0.62rem; + font-weight: 700; + border-radius: 7px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 5px; + } + .section-body { + padding: 0 0.5rem 0.5rem 0.5rem; + margin-bottom: 0.5rem; + flex: 1; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; + } + .section-body::-webkit-scrollbar { + width: 5px; + } + .section-body::-webkit-scrollbar-track { + background: transparent; + } + .section-body::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 10px; + } + .section-body::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); + } + .resource-search { + border: 1.5px solid var(--border); + border-radius: var(--radius-sm); + padding: 0.35rem 0.65rem; + font-size: 0.74rem; + background: var(--bg); + color: var(--text-primary); + outline: none; + width: 135px; + transition: + border-color 0.25s, + box-shadow 0.25s, + width 0.25s; + } + .resource-search:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); + width: 175px; + } + .resource-search::placeholder { + color: var(--text-muted); + } + + /* Resource item */ + .res-item { + position: relative; + overflow: hidden; + border-radius: var(--radius-lg); + margin: 0.2rem 0; + transition: all 0.3s cubic-bezier(0.22, 1, 0.36, 1); + cursor: default; + opacity: 0; + animation: itemFadeIn 0.4s ease forwards; + } + .res-item:hover { + box-shadow: var(--shadow-sm); + transform: translateY(-2px); + } + .res-item:active { + transform: translateY(0); + } + .res-bg { + position: absolute; + inset: -40px; + background-size: cover; + background-position: center; + filter: blur(50px) saturate(1.3); + opacity: 0.07; + z-index: 0; + transition: opacity 0.35s; + } + .res-item:hover .res-bg { + opacity: 0.15; + } + .res-row { + position: relative; + z-index: 1; + display: flex; + align-items: center; + padding: 0.6rem 0.75rem; + } + .res-cover { + width: 44px; + height: 44px; + border-radius: 11px; + object-fit: cover; + flex-shrink: 0; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12); + } + .res-cover-ph { + width: 44px; + height: 44px; + border-radius: 11px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + background: var(--bg); + font-size: 1rem; + border: 1px solid var(--border-light); + } + .res-meta { + flex: 1; + min-width: 0; + margin: 0 0.65rem; + } + .res-title { + font-weight: 700; + font-size: 0.83rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + letter-spacing: -0.01em; + } + .res-artist { + font-size: 0.71rem; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 0.08rem; + } + .res-actions { + display: flex; + align-items: center; + gap: 0.25rem; + flex-shrink: 0; + } + .heat-chip { + font-size: 0.62rem; + font-weight: 600; + background: var(--warning-light); + color: var(--warning); + padding: 0.14rem 0.4rem; + border-radius: 7px; + display: inline-flex; + align-items: center; + gap: 0.18rem; + } + .btn-icon { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: none; + color: var(--text-muted); + font-size: 1rem; + border-radius: 9px; + transition: all 0.2s; + } + .btn-icon:hover { + background: var(--accent-light); + color: var(--accent); + } + .btn-icon:active { + transform: scale(0.85); + } + .btn-icon.danger:hover { + background: var(--danger-light); + color: var(--danger); + } + .res-detail { + position: relative; + z-index: 1; + max-height: 0; + opacity: 0; + overflow: hidden; + transition: + max-height 0.35s cubic-bezier(0.22, 1, 0.36, 1), + opacity 0.3s, + padding 0.35s; + font-size: 0.68rem; + color: var(--text-secondary); + padding: 0 0.75rem 0 calc(0.75rem + 44px + 0.65rem); + } + .res-item:hover .res-detail { + max-height: 36px; + opacity: 1; + padding-bottom: 0.45rem; + } + .res-detail span { + display: inline-flex; + align-items: center; + gap: 0.15rem; + } + .detail-sep { + color: var(--border); + margin: 0 0.3rem; + } + + /* Playlist item */ + .pl-item { + position: relative; + display: flex; + align-items: center; + gap: 0.55rem; + padding: 0.6rem 0.75rem; + border-radius: var(--radius-lg); + margin: 0.2rem 0; + transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1); + cursor: grab; + opacity: 0; + animation: itemFadeIn 0.4s ease forwards; + } + .pl-item:active { + cursor: grabbing; + } + .pl-item:hover { + background: var(--bg); + transform: translateY(-1px); + } + .pl-grip { + color: var(--text-muted); + font-size: 0.8rem; + flex-shrink: 0; + opacity: 0.3; + transition: opacity 0.2s; + } + .pl-item:hover .pl-grip { + opacity: 0.7; + } + .pl-num { + width: 20px; + height: 20px; + background: var(--accent-light); + color: var(--accent); + font-size: 0.6rem; + font-weight: 700; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-family: "JetBrains Mono", monospace; + } + .pl-info { + flex: 1; + min-width: 0; + } + .pl-name { + font-weight: 700; + font-size: 0.82rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + letter-spacing: -0.01em; + } + .pl-sub { + display: flex; + align-items: center; + gap: 0.4rem; + margin-top: 0.12rem; + } + .pl-cache { + font-size: 0.6rem; + font-weight: 600; + display: inline-flex; + align-items: center; + gap: 0.18rem; + padding: 0.08rem 0.35rem; + border-radius: 6px; + } + .pl-cache.ready { + background: var(--success-light); + color: var(--success); + } + .pl-cache.pending { + background: var(--warning-light); + color: var(--warning); + } + .pl-cache.none { + background: var(--bg); + color: var(--text-muted); + } + .pl-heat { + font-size: 0.6rem; + } + .btn-clear { + border: none; + background: var(--danger-light); + color: var(--danger); + font-size: 0.68rem; + font-weight: 700; + padding: 0.3rem 0.7rem; + border-radius: 10px; + transition: all 0.2s; + display: inline-flex; + align-items: center; + gap: 0.25rem; + } + .btn-clear:hover { + background: #fdd; + box-shadow: 0 2px 8px rgba(239, 69, 101, 0.15); + } + .btn-clear:active { + transform: scale(0.97); + } + + /* Sortable */ + .sortable-ghost { + opacity: 0.3; + background: var(--accent-light) !important; + border-radius: var(--radius-lg); + } + + /* ==================== ABOUT PAGE ==================== */ + .about-card { + display: flex; + flex-direction: column; + min-height: 0; + } + .about-hero { + text-align: center; + padding: 2.2rem 1.5rem 1.8rem; + background: linear-gradient( + 135deg, + var(--accent-light) 0%, + rgba(139, 92, 246, 0.06) 100% + ); + flex-shrink: 0; + border-radius: var(--radius-xl) var(--radius-xl) 0 0; + } + .about-logo { + width: 72px; + height: 72px; + border-radius: 18px; + overflow: hidden; + margin: 0 auto 1rem; + box-shadow: 0 4px 24px rgba(79, 110, 247, 0.25); + } + .about-logo img { + width: 100%; + height: 100%; + object-fit: cover; + } + .about-hero h2 { + font-size: 1.25rem; + font-weight: 800; + letter-spacing: -0.03em; + margin-bottom: 0.15rem; + } + .about-hero p { + font-size: 0.82rem; + color: var(--text-secondary); + } + .about-body { + padding: 0.5rem 1.5rem 1.8rem; + flex: 1; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; + } + .about-body::-webkit-scrollbar { + width: 5px; + } + .about-body::-webkit-scrollbar-track { + background: transparent; + } + .about-body::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 10px; + } + .about-body::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); + } + .about-section { + display: flex; + gap: 0.75rem; + padding: 0.9rem 0; + border-bottom: 1px solid var(--border-light); + } + .about-section:last-child { + border-bottom: none; + } + .about-section-icon { + width: 34px; + height: 34px; + background: var(--accent-light); + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + color: var(--accent); + font-size: 0.95rem; + flex-shrink: 0; + } + .about-section-content h3 { + font-size: 0.82rem; + font-weight: 700; + margin-bottom: 0.25rem; + } + .about-section-content p { + font-size: 0.76rem; + color: var(--text-secondary); + line-height: 1.7; + } + .tech-tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + } + .tech-tags span { + font-size: 0.68rem; + font-weight: 600; + background: var(--bg); + color: var(--text-secondary); + padding: 0.2rem 0.55rem; + border-radius: 8px; + border: 1px solid var(--border-light); + } + .disclaimer-section .about-section-icon { + background: var(--warning-light); + color: var(--warning); + } + .about-version { + text-align: center; + padding: 0.8rem; + font-size: 0.65rem; + color: var(--text-muted); + font-family: "JetBrains Mono", monospace; + letter-spacing: -0.01em; + flex-shrink: 0; + } + + /* ==================== ABOUT — EXTENSIONS ==================== */ + /* 核心功能列表 */ + .about-feature-list { + display: flex; + flex-direction: column; + gap: 0.35rem; + margin-top: 0.2rem; + } + + .about-feat { + display: flex; + align-items: flex-start; + gap: 0.45rem; + font-size: 0.76rem; + color: var(--text-secondary); + line-height: 1.65; + padding: 0.3rem 0; + } + + .about-feat i { + font-size: 0.72rem; + flex-shrink: 0; + margin-top: 0.18rem; + } + + /* GitHub 链接徽章 */ + .about-gh-link { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.3rem 0.7rem; + background: var(--bg); + border: 1px solid var(--border-light); + border-radius: 10px; + font-size: 0.72rem; + font-weight: 600; + color: var(--text-secondary); + text-decoration: none; + transition: all 0.2s; + font-family: "JetBrains Mono", monospace; + letter-spacing: -0.01em; + } + + .about-gh-link:hover { + color: var(--accent); + border-color: var(--accent); + background: var(--accent-light); + } + + .about-gh-link i:first-child { + font-size: 0.9rem; + } + + /* ==================== MINI PLAYER ==================== */ + .mini-player { + position: fixed; + bottom: calc(var(--nav-bottom) + var(--nav-h) + 12px); + left: 50%; + transform: translateX(-50%); + width: min(var(--content-w), calc(100% - 2rem)); + z-index: 99; + border-radius: 20px; + overflow: hidden; + background: rgba(255, 255, 255, 0.85); + backdrop-filter: blur(28px) saturate(200%); + border: 1px solid rgba(255, 255, 255, 0.6); + box-shadow: + 0 4px 24px rgba(15, 18, 37, 0.1), + 0 1px 4px rgba(15, 18, 37, 0.04), + inset 0 1px 0 rgba(255, 255, 255, 0.8); + opacity: 0; + pointer-events: none; + transform: translateX(-50%) translateY(16px) scale(0.96); + transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); + } + .mini-player.show { + opacity: 1; + pointer-events: auto; + transform: translateX(-50%) translateY(0) scale(1); + } + /* progress = full background */ + .mp-progress { + position: absolute; + inset: 0; + width: 0; + background: linear-gradient( + 90deg, + rgba(79, 110, 247, 0.11), + rgba(139, 92, 246, 0.08) + ); + pointer-events: none; + z-index: 0; + transition: width 0.15s linear; + } + .mini-player:hover .mp-progress { + background: linear-gradient( + 90deg, + rgba(79, 110, 247, 0.16), + rgba(139, 92, 246, 0.12) + ); + } + .mp-body { + position: relative; + z-index: 1; + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.5rem 0.9rem 0.55rem; + cursor: pointer; + user-select: none; + -webkit-user-select: none; + } + .mp-cover { + width: 40px; + height: 40px; + border-radius: 9px; + object-fit: cover; + flex-shrink: 0; + box-shadow: var(--shadow-xs); + } + .mp-info { + flex: 1; + min-width: 0; + } + .mp-name { + font-weight: 700; + font-size: 0.8rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + letter-spacing: -0.01em; + } + .mp-artist { + font-size: 0.68rem; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 0.04rem; + } + .mp-controls { + display: flex; + align-items: center; + gap: 0.3rem; + flex-shrink: 0; + } + .mp-time { + font-family: "JetBrains Mono", monospace; + font-size: 0.64rem; + color: var(--text-muted); + flex-shrink: 0; + min-width: 40px; + text-align: center; + } + .mp-play { + width: 34px; + height: 34px; + border: none; + border-radius: 50%; + background: var(--accent); + color: #fff; + font-size: 0.95rem; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + } + .mp-play:hover { + background: var(--accent-hover); + box-shadow: 0 2px 8px rgba(79, 110, 247, 0.3); + } + .mp-play:active { + transform: scale(0.88); + } + .mp-expand { + width: 30px; + height: 30px; + border: none; + border-radius: 50%; + background: transparent; + color: var(--text-muted); + font-size: 0.85rem; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + } + .mp-expand:hover { + background: var(--accent-light); + color: var(--accent); + } + + /* ==================== BOTTOM NAV ==================== */ + .bottom-nav { + position: fixed; + bottom: var(--nav-bottom); + left: 50%; + transform: translateX(-50%); + z-index: 100; + pointer-events: none; + animation: navSlideUp 0.5s cubic-bezier(0.22, 1, 0.36, 1) 0.15s + both; + } + @keyframes navSlideUp { + from { + opacity: 0; + transform: translateX(-50%) translateY(24px); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0); + } + } + .bottom-nav-inner { + pointer-events: auto; + display: inline-flex; + align-items: center; + gap: 4px; + padding: 6px 8px; + background: rgba(255, 255, 255, 0.72); + backdrop-filter: blur(32px) saturate(220%); + -webkit-backdrop-filter: blur(32px) saturate(220%); + border: 1px solid rgba(255, 255, 255, 0.55); + border-radius: 22px; + box-shadow: + 0 2px 12px rgba(15, 18, 37, 0.07), + 0 0 0 0.5px rgba(15, 18, 37, 0.03), + inset 0 1px 0 rgba(255, 255, 255, 0.9); + } + .nav-tab { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + padding: 8px 18px; + border: none; + background: none; + color: var(--text-muted); + font-size: 0.56rem; + font-weight: 600; + letter-spacing: 0.03em; + border-radius: 16px; + z-index: 1; + transition: + color 0.3s ease, + transform 0.2s ease; + } + .nav-tab i { + font-size: 1.25rem; + transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); + position: relative; + z-index: 1; + line-height: 1; + } + .nav-tab span { + position: relative; + z-index: 1; + line-height: 1; + } + /* active pill */ + .nav-tab::before { + content: ""; + position: absolute; + inset: 0; + background: var(--accent); + border-radius: 16px; + opacity: 0; + transform: scale(0.6); + transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); + z-index: 0; + } + .nav-tab.active { + color: #fff; + } + .nav-tab.active::before { + opacity: 1; + transform: scale(1); + } + .nav-tab.active i { + transform: scale(1.1) translateY(-1px); + } + .nav-tab:hover:not(.active) { + color: var(--text-secondary); + } + .nav-tab:hover:not(.active)::before { + opacity: 0.04; + transform: scale(0.85); + background: var(--accent); + } + .nav-tab:active { + transform: scale(0.92); + } + + /* ==================== EMPTY / SCROLL ==================== */ + .empty-state { + text-align: center; + padding: 2rem 1.5rem; + color: var(--text-muted); + } + .empty-icon { + width: 48px; + height: 48px; + background: var(--bg); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 0.75rem; + font-size: 1.1rem; + color: var(--text-muted); + border: 1px solid var(--border-light); + animation: float 3.5s ease-in-out infinite; + } + .empty-title { + font-weight: 700; + font-size: 0.82rem; + color: var(--text-secondary); + margin-bottom: 0.15rem; + } + .empty-desc { + font-size: 0.74rem; + } + + /* ==================== TOAST ==================== */ + .toast-container { + position: fixed; + top: 68px; + left: 50%; + transform: translateX(-50%); + z-index: 9999; + display: flex; + flex-direction: column; + gap: 0.4rem; + pointer-events: none; + } + .toast-msg { + background: var(--text-primary); + color: #fff; + padding: 0.5rem 1.1rem; + border-radius: var(--radius-sm); + font-size: 0.78rem; + font-weight: 600; + box-shadow: var(--shadow-lg); + animation: + toastIn 0.35s cubic-bezier(0.22, 1, 0.36, 1) forwards, + toastOut 0.3s ease 2.5s forwards; + pointer-events: auto; + white-space: nowrap; + letter-spacing: -0.01em; + } + .toast-msg.error { + background: var(--danger); + } + .toast-msg.success { + background: var(--success); + } + @keyframes toastIn { + from { + opacity: 0; + transform: translateY(-10px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } + } + @keyframes toastOut { + from { + opacity: 1; + } + to { + opacity: 0; + transform: translateY(-8px) scale(0.95); + } + } + + /* ==================== ANIMATIONS ==================== */ + @keyframes itemFadeIn { + from { + opacity: 0; + transform: translateY(7px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + @keyframes itemFadeOut { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(-5px); + } + } + .res-item.enter, + .pl-item.enter { + animation: itemFadeIn 0.35s ease forwards; + } + .res-item.leave, + .pl-item.leave { + animation: itemFadeOut 0.25s ease forwards; + } + .spinner-sm { + width: 15px; + height: 15px; + border: 2.5px solid rgba(255, 255, 255, 0.3); + border-top-color: #fff; + border-radius: 50%; + animation: spin 0.6s linear infinite; + display: inline-block; + } + @keyframes spin { + to { + transform: rotate(360deg); + } + } + + /* ==================== RESPONSIVE ==================== */ + /* Tablet */ + @media (min-width: 768px) { + .page-container { + padding: 1.2rem 2rem 2rem; + padding-bottom: calc(var(--total-bottom) + 2rem); + } + .res-detail { + display: block; + } + .search-header { + padding: 1.8rem 2rem 0.4rem; + } + .search-input-wrap { + padding: 0.8rem 2rem; + } + .preview-card { + margin: 0 2rem 2rem; + } + .searchResults { + margin: 0 2rem 2rem; + } + .parse-hint { + padding: 0 2rem; + } + .search-tips { + padding: 2.5rem 2rem 3rem; + } + .about-body { + padding: 0.5rem 2rem 2rem; + } + } + /* Large desktop */ + @media (min-width: 1200px) { + :root { + --content-w: 960px; + } + .mini-player { + width: min(960px, calc(100% - 3rem)); + } + } + /* Small phones */ + @media (max-width: 380px) { + :root { + --nav-bottom: 10px; + } + .nav-tab { + font-size: 0.54rem; + } + .nav-tab i { + font-size: 1.05rem; + } + .cache-pill { + display: none; + } + .search-header { + padding: 1.1rem 1.1rem 0.25rem; + } + .search-input-wrap { + padding: 0.6rem 1.1rem; + } + .preview-card { + margin: 0 1.1rem 1.1rem; + } + .searchResults { + margin: 0 1.1rem 1.1rem; + } + .parse-hint { + padding: 0 1.1rem; + } + .about-body { + padding: 0.4rem 1.1rem 1.2rem; + } + } + /* Landscape phone */ + @media (max-height: 500px) and (orientation: landscape) { + .top-header { + padding: 0.3rem 0.8rem; + } + .page-container { + padding: 0.5rem 1rem 1rem; + } + .search-tips { + padding: 0.8rem; + } + .tips-icon { + width: 40px; + height: 40px; + font-size: 0.9rem; + margin-bottom: 0.5rem; + } + .preview-cover-section { + padding: 0.6rem 0.8rem; + } + .preview-cover, + .preview-cover-wrap { + width: 48px; + height: 48px; + } + .sr-list, + .scroll-area { + max-height: 38vh; + } + .about-hero { + padding: 1.2rem 1rem 1rem; + } + .about-logo { + width: 52px; + height: 52px; + } + } + /* Reduced motion */ + @media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } + } diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..610ec3f9b2ed83d288acfa2e43af2a489cf1d7f3 GIT binary patch literal 270398 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 literal 0 HcmV?d00001 diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..b818748f4df9bdc0afe7004da19265b888fefc48 GIT binary patch literal 98344 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 + + + + + + Xiao の 电台 + + + + + + + + + + +
+
+
+
+
+
+
+ + +
+
+
+
+ logo +
+ Xiao の 电台 + LIVE +
+
+ -- + 直播流 +
+
+
+ + +
+ +
+
+
+

导入音源

+

输入网易云歌曲 ID、链接或歌名查询并下载

+
+
+
+ + +
+
+
+ + 已自动解析为 ID: +
+ + + +
+
+ +
+
搜索并下载音乐
+
+ 在网易云音乐中复制歌曲 ID 或完整链接
粘贴到上方输入框即可自动识别 +
+
+
+
+ + +
+
+
+
+ + 已有音乐 + 0 +
+ +
+
+
+
+ +
+
加载中...
+
+
+
+
+ + +
+
+
+
+ + 播放列表 + 0 +
+ +
+
+
+
+ +
+
加载中...
+
+
+
+
+ + +
+
+ +
+ +

Xiao の 电台

+

基于 HLS 的个人在线音乐电台

+
+ + +
+ +
+
+ +
+
+

项目简介

+

+ 一个基于 Web 的个人在线音乐电台,支持通过网易云音乐歌曲 ID、链接或歌名进行搜索、试听与下载,提供 HLS 直播流播放、资源管理与播放列表拖拽排序等功能。界面采用简约玻璃拟态风格,适配移动端与桌面端多种屏幕尺寸。 +

+
+
+ + +
+
+ +
+
+

核心功能

+
+
+ + 智能搜索 — 歌曲 ID、链接或歌名关键词,自动解析并一键导入 +
+
+ + 直播流 — FFmpeg HLS 切片 + 滑动窗口,5 秒精度连续输出 +
+
+ + 智能缓存 — 预生成切片、增量追踪、LRU 淘汰与自动清理 +
+
+ + 播放列表 — 拖拽排序、添加/移除/跳转,SSE 实时状态推送 +
+
+ + 全端适配 — 玻璃拟态 UI,手势滑动、键盘快捷键、拖拽进度条 +
+
+
+
+ + +
+
+ +
+
+

技术栈

+
+ HTML5 + CSS3 + JavaScript ES6+ + Bootstrap 5 + SortableJS + HLS.js + Node.js + Express + FFmpeg + Chokidar +
+
+
+ + +
+
+ +
+
+

开发语言

+

前端:HTML / CSS / JavaScript  |  后端:Node.js

+
+
+ + +
+
+ +
+
+

作者

+

Xiao — 独立开发者

+ + + Xiao-no-love + + +
+
+ + +
+
+ +
+
+

免责声明

+

+ 本项目所有音源均来自网易云音乐,仅供个人学习与技术研究使用,严禁用于任何商业用途。音乐版权归原创作者及网易云音乐平台所有。如有侵权,请联系删除。请于下载后 24 小时内清除,支持正版音乐。 +

+
+
+
+ + +
+ v1.0.0 · Built with ♥ · + GitHub +
+
+
+
+ + +
+
+
+ +
+
未在播放
+
--
+
+
+ 0:00 + + +
+
+
+ + + + + + + + + + diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 0000000..5d02f4a --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,758 @@ + /* ============================================================ + Xiao の 电台 — Enhanced JS + ============================================================ */ + + // ==================== Toast ==================== + function showToast(msg, type = "") { + const c = document.getElementById("toastContainer"); + const el = document.createElement("div"); + el.className = "toast-msg" + (type ? " " + type : ""); + el.textContent = msg; + c.appendChild(el); + setTimeout(() => el.remove(), 3000); + } + + // ==================== Navigation ==================== + const tabs = document.querySelectorAll(".nav-tab"); + const pages = document.querySelectorAll(".page"); + + function switchPage(pageId) { + pages.forEach((p) => p.classList.remove("active")); + tabs.forEach((t) => t.classList.remove("active")); + const page = document.getElementById(pageId); + if (page) page.classList.add("active"); + tabs.forEach((t) => { + if (t.dataset.page === pageId) t.classList.add("active"); + }); + // Eager-load data on tab switch + if (pageId === "pageResources") loadResources(); + if (pageId === "pagePlaylist") loadPlaylist(); + } + tabs.forEach((tab) => + tab.addEventListener("click", () => + switchPage(tab.dataset.page), + ), + ); + + // ==================== Helpers ==================== + function esc(s) { + return String(s ?? "").replace( + /[&<>"']/g, + (c) => + ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + })[c], + ); + } + function formatDuration(sec) { + if (!sec && sec !== 0) return "--:--"; + const s = Math.round(sec), + m = Math.floor(s / 60), + ss = s % 60; + return m + ":" + String(ss).padStart(2, "0"); + } + function formatBytes(b) { + if (!b) return "0 B"; + if (b >= 1048576) return (b / 1048576).toFixed(2) + " MB"; + if (b >= 1024) return (b / 1024).toFixed(1) + " KB"; + return b + " B"; + } + async function api(url, options = {}) { + const res = await fetch(url, options); + if (!res.ok) throw new Error(await res.text()); + return res.json(); + } + + // ==================== ID Extraction ==================== + function extractMusicId(input) { + input = input.trim(); + if (!input) return ""; + if (/^\d+$/.test(input)) return input; + const patterns = [ + /music\.163\.com\/#\/song\?.*id=(\d+)/, + /music\.163\.com\/song\?.*id=(\d+)/, + /music\.163\.com\/song\/(\d+)/, + /y\.music\.163\.com\/m\/song\?.*id=(\d+)/, + /[?&]id=(\d+)/, + /(\d{5,})/, + ]; + for (const p of patterns) { + const m = input.match(p); + if (m) return m[1]; + } + return input; + } + + // ==================== Parse Hint ==================== + const musicIdInput = document.getElementById("musicId"); + const parseHint = document.getElementById("parseHint"); + const parsedIdEl = document.getElementById("parsedId"); + function updateParseHint() { + const val = musicIdInput.value.trim(), + extracted = extractMusicId(val); + if (val && extracted !== val && /^\d+$/.test(extracted)) { + parsedIdEl.textContent = extracted; + parseHint.classList.add("visible"); + } else { + parseHint.classList.remove("visible"); + } + } + musicIdInput.addEventListener("input", updateParseHint); + musicIdInput.addEventListener("paste", () => + requestAnimationFrame(updateParseHint), + ); + + // ==================== List Diff ==================== + function reconcileList(container, items, keyOf, create, update) { + const nodes = Array.from( + container.querySelectorAll("[data-key]"), + ); + const byKey = new Map(nodes.map((n) => [n.dataset.key, n])); + const seen = new Set(); + items.forEach((item, idx) => { + const key = String(keyOf(item)); + seen.add(key); + let el = byKey.get(key); + if (!el) { + el = create(item); + el.dataset.key = key; + el.classList.add("enter"); + } + update(el, item, idx); + if (container.children[idx] !== el) + container.insertBefore( + el, + container.children[idx] || null, + ); + }); + nodes.forEach((el) => { + if (!seen.has(el.dataset.key)) { + el.classList.add("leave"); + el.addEventListener("animationend", () => el.remove(), { + once: true, + }); + } + }); + } + + // ==================== Resources ==================== + function renderResItem(item) { + const cover = item.albumImage + ? `` + : `
`; + const bg = item.albumImage + ? `
` + : ""; + const tpl = document.createElement("template"); + tpl.innerHTML = `
${bg} +
${cover} +
+
${esc(item.title)}
+
${esc(item.artist) || "未知作者"}
+
+
+ ${item.heat} + +
+
+
+ ${formatDuration(item.duration)} + | + ${formatBytes(item.fileSize)} + | + 缓存 ${formatBytes(item.cacheSize)} +
+
`; + return tpl.content.firstElementChild; + } + function updateResItem(el, item) { + const h = el.querySelector(".js-heat"); + if (h.textContent !== String(item.heat)) + h.textContent = item.heat; + const c = el.querySelector(".js-cache"), + cs = formatBytes(item.cacheSize); + if (c.textContent !== cs) c.textContent = cs; + } + let resourceKeyword = ""; + function applyResourceFilter() { + document + .getElementById("resourceList") + .querySelectorAll(".res-item") + .forEach((el) => { + const t = ( + el.querySelector(".res-title")?.textContent || "" + ).toLowerCase(); + const a = ( + el.querySelector(".res-artist")?.textContent || "" + ).toLowerCase(); + el.style.display = + !resourceKeyword || + t.includes(resourceKeyword) || + a.includes(resourceKeyword) + ? "" + : "none"; + }); + } + async function loadResources() { + try { + const resources = await api("/api/resources"); + const container = document.getElementById("resourceList"); + document.getElementById("resourceCount").textContent = + resources.length; + if (resources.length === 0) { + container.innerHTML = `
暂无资源
前往搜索页导入音乐
`; + return; + } + if (!container.querySelector("[data-key]")) + container.innerHTML = ""; + reconcileList( + container, + resources, + (r) => r.file, + renderResItem, + updateResItem, + ); + applyResourceFilter(); + } catch (err) { + console.error("加载资源失败:", err); + } + } + + // ==================== Playlist ==================== + let sortableInstance = null; + function renderPlItem(item) { + const tpl = document.createElement("template"); + tpl.innerHTML = `
+ + +
+
${esc(item.name)}
+
+ + +
+
+ +
`; + return tpl.content.firstElementChild; + } + function updatePlItem(el, item, idx) { + const numEl = el.querySelector(".pl-num"); + if (numEl.textContent !== String(idx + 1)) + numEl.textContent = idx + 1; + const heatEl = el.querySelector(".js-heat"); + if (heatEl.textContent !== String(item.heat)) + heatEl.textContent = item.heat; + const cacheEl = el.querySelector(".pl-cache"); + const st = + item.cacheStatus === "ready" + ? "ready" + : item.cacheStatus === "pending" + ? "pending" + : "none"; + if (cacheEl.dataset.status !== st) { + cacheEl.dataset.status = st; + cacheEl.className = "pl-cache " + st; + cacheEl.innerHTML = + st === "ready" + ? ' 已缓存' + : st === "pending" + ? ' 生成中' + : ' 未缓存'; + } + el.querySelector("button").setAttribute( + "onclick", + "removeFromPlaylist(" + idx + ")", + ); + } + async function loadPlaylist() { + try { + const playlistData = await api("/api/playlist"); + const container = document.getElementById("playlistList"); + document.getElementById("playlistCount").textContent = + playlistData.length; + if (playlistData.length === 0) { + container.innerHTML = `
播放列表为空
从资源列表中添加音乐
`; + if (sortableInstance) { + sortableInstance.destroy(); + sortableInstance = null; + } + return; + } + if (!container.querySelector("[data-key]")) + container.innerHTML = ""; + reconcileList( + container, + playlistData, + (p) => p.path, + renderPlItem, + updatePlItem, + ); + if (!sortableInstance) { + sortableInstance = new Sortable(container, { + animation: 200, + ghostClass: "sortable-ghost", + handle: ".pl-grip", + delay: 100, + delayOnTouchOnly: true, + onEnd: async () => { + const items = [ + ...container.querySelectorAll(".pl-item"), + ]; + const newOrder = items.map( + (el) => el.dataset.id, + ); + try { + await api("/api/playlist/reorder", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ newOrder }), + }); + items.forEach((el, i) => { + el.querySelector( + ".pl-num", + ).textContent = i + 1; + el.querySelector("button").setAttribute( + "onclick", + "removeFromPlaylist(" + i + ")", + ); + }); + } catch (err) { + console.error("排序更新失败:", err); + loadPlaylist(); + } + }, + }); + } + } catch (err) { + console.error("加载播放列表失败:", err); + } + } + + // ==================== Actions ==================== + async function addToPlaylist(fileName) { + try { + await api("/api/playlist/add", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ file: fileName }), + }); + showToast("已添加到播放列表", "success"); + await loadPlaylist(); + await loadResources(); + } catch (err) { + showToast("添加失败: " + err.message, "error"); + } + } + async function removeFromPlaylist(index) { + try { + await api("/api/playlist/remove", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ index }), + }); + await loadPlaylist(); + } catch (err) { + showToast("移除失败: " + err.message, "error"); + } + } + async function clearPlaylist() { + if (!confirm("确定清空播放列表?清空后将自动随机添加一首。")) + return; + try { + await api("/api/playlist/clear", { method: "POST" }); + await loadPlaylist(); + } catch (err) { + showToast("清空失败: " + err.message, "error"); + } + } + + // ==================== Cache Info ==================== + async function loadCacheInfo() { + try { + const info = await api("/api/cache/size"); + document.getElementById("cacheInfo").innerHTML = + ` ${(info.total / 1048576).toFixed(1)}/${(info.limit / 1048576).toFixed(0)}MB (${info.usedPercent}%)`; + } catch (err) { + console.error("加载缓存信息失败:", err); + } + } + + // ==================== Mini Player ==================== // + const musicPlayer = document.getElementById("musicPlayer"); + const miniPlayer = document.getElementById("miniPlayer"); + const mpBody = document.getElementById("mpBody"); + const mpProgress = document.getElementById("mpProgress"); + const mpCover = document.getElementById("mpCover"); + const mpName = document.getElementById("mpName"); + const mpArtist = document.getElementById("mpArtist"); + const mpPlay = document.getElementById("mpPlay"); + const mpTime = document.getElementById("mpTime"); + const mpExpand = document.getElementById("mpExpand"); + + function showMiniPlayer(info) { + mpName.textContent = info.name || "未知"; + mpArtist.textContent = info.artist || "--"; + if (info.albumImage) { + mpCover.src = info.albumImage; + mpCover.style.display = ""; + } else { + mpCover.style.display = "none"; + } + miniPlayer.classList.add("show"); + document.body.classList.add("mini-player-visible"); + } + function hideMiniPlayer() { + miniPlayer.classList.remove("show"); + document.body.classList.remove("mini-player-visible"); + } + + mpPlay.addEventListener("click", () => { + if (musicPlayer.paused) musicPlayer.play(); + else musicPlayer.pause(); + }); + musicPlayer.addEventListener("play", () => { + mpPlay.innerHTML = ''; + }); + musicPlayer.addEventListener("pause", () => { + mpPlay.innerHTML = ''; + }); + musicPlayer.addEventListener("timeupdate", () => { + if (!musicPlayer.duration) return; + const pct = + (musicPlayer.currentTime / musicPlayer.duration) * 100; + mpProgress.style.width = pct + "%"; + mpTime.textContent = formatDuration(musicPlayer.currentTime); + }); + musicPlayer.addEventListener("ended", () => { + mpPlay.innerHTML = ''; + }); + + mpExpand.addEventListener("click", () => { + switchPage("pageSearch"); + const preview = document.getElementById("musicPreview"); + if (preview.style.display !== "none") + preview.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + }); + + // ---- Drag-to-seek on the entire mini player bar ---- + let isSeeking = false; + function seekFromPointer(e) { + const rect = mpBody.getBoundingClientRect(); + const x = e.touches ? e.touches[0].clientX : e.clientX; + const ratio = Math.max( + 0, + Math.min(1, (x - rect.left) / rect.width), + ); + if (musicPlayer.duration) { + musicPlayer.currentTime = ratio * musicPlayer.duration; + mpProgress.style.width = ratio * 100 + "%"; + } + } + mpBody.addEventListener("mousedown", (e) => { + if (e.target.closest("button")) return; + isSeeking = true; + seekFromPointer(e); + }); + mpBody.addEventListener( + "touchstart", + (e) => { + if (e.target.closest("button")) return; + isSeeking = true; + seekFromPointer(e); + }, + { passive: true }, + ); + document.addEventListener("mousemove", (e) => { + if (isSeeking) seekFromPointer(e); + }); + document.addEventListener( + "touchmove", + (e) => { + if (isSeeking) seekFromPointer(e); + }, + { passive: true }, + ); + document.addEventListener("mouseup", () => { + isSeeking = false; + }); + document.addEventListener("touchend", () => { + isSeeking = false; + }); + + // ==================== Music Search ==================== + let musicInfo = null, + selectedId = null; + + async function showDetail(id) { + const info = await api("/api/music/detail", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id }), + }); + musicInfo = info; + selectedId = id; + document.getElementById("musicName").textContent = info.name; + document.getElementById("musicArtist").textContent = + info.artist || "未知作者"; + document.getElementById("musicCover").src = + info.albumImage || ""; + + let tags = ""; + if (info.album) + tags += ` ${esc(info.album)}`; + if (info.duration) + tags += ` ${formatDuration(info.duration)}`; + if (info.quality) + tags += ` ${esc(info.quality)}`; + if (info.fileSize) + tags += ` ${formatBytes(info.fileSize)}`; + if (info.publishTime) { + const d = new Date(info.publishTime); + tags += ` ${d.getFullYear()}.${String(d.getMonth() + 1).padStart(2, "0")}.${String(d.getDate()).padStart(2, "0")}`; + } + if (id) + tags += ` ${id}`; + document.getElementById("musicDetails").innerHTML = tags; + + document.getElementById("musicPreview").style.display = "block"; + document.getElementById("searchTips").style.display = "none"; + + musicPlayer.pause(); + musicPlayer.src = + "http://music.163.com/song/media/outer/url?id=" + + id + + ".mp3"; + + showMiniPlayer({ + name: info.name, + artist: info.artist, + albumImage: info.albumImage, + }); + mpProgress.style.width = "0%"; + mpTime.textContent = "0:00"; + } + + function renderSearchResults(data, keyword) { + const container = document.getElementById("searchResults"); + musicInfo = null; + selectedId = null; + document.getElementById("musicPreview").style.display = "none"; + + if (!data.list || data.list.length === 0) { + container.innerHTML = `
未找到相关歌曲
换个关键词试试
`; + container.style.display = "block"; + document.getElementById("searchTips").style.display = + "none"; + return; + } + + container.innerHTML = ` +
"${esc(keyword)}" 共 ${data.total} 首相关歌曲,点击选择:
+
${data.list + .map( + (item, i) => ` +
+
+
${esc(item.name)}
+
${esc(item.artists.join(" / ") || "未知作者")}
+
+ ${item.alias && item.alias[0] ? `
${esc(item.alias[0])}
` : ""} +
${formatDuration(item.duration / 1000)}
+
`, + ) + .join("")}
`; + container.style.display = "block"; + document.getElementById("searchTips").style.display = "none"; + + container.querySelectorAll(".sr-item").forEach((el) => { + el.addEventListener("click", () => { + container + .querySelectorAll(".sr-item") + .forEach((x) => x.classList.remove("active")); + el.classList.add("active"); + showDetail(el.dataset.id).catch((err) => + showToast("查询失败: " + err.message, "error"), + ); + }); + }); + } + + async function searchMusic() { + const rawInput = musicIdInput.value.trim(); + if (!rawInput) + return showToast("请输入歌曲 ID、链接或歌名", "error"); + const id = extractMusicId(rawInput); + const btn = document.getElementById("btnSearch"); + btn.disabled = true; + btn.textContent = "查询中..."; + try { + if (id && /^\d+$/.test(id)) { + document.getElementById("searchResults").style.display = + "none"; + await showDetail(id); + } else { + renderSearchResults( + await api( + "/api/music/search?keyword=" + + encodeURIComponent(rawInput), + ), + rawInput, + ); + } + } catch (err) { + showToast("查询失败: " + err.message, "error"); + } finally { + btn.disabled = false; + btn.textContent = "查询"; + } + } + + async function downloadMusic() { + if (!musicInfo || !selectedId) return; + const btn = document.getElementById("btnDownload"); + btn.disabled = true; + btn.innerHTML = ' 下载中'; + try { + const r = await api("/api/music/download", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + id: selectedId, + name: musicInfo.name, + artist: musicInfo.artist, + albumImage: musicInfo.albumImage, + }), + }); + musicIdInput.value = ""; + musicInfo = null; + selectedId = null; + document.getElementById("musicPreview").style.display = + "none"; + document.getElementById("searchResults").style.display = + "none"; + document.getElementById("searchTips").style.display = + "block"; + parseHint.classList.remove("visible"); + loadResources(); + showToast("已下载: " + r.file, "success"); + } catch (err) { + showToast("下载失败: " + err.message, "error"); + } finally { + btn.disabled = false; + btn.innerHTML = + ' 下载到资源库'; + } + } + + // ==================== Event Listeners ==================== + document + .getElementById("btnSearch") + .addEventListener("click", searchMusic); + document + .getElementById("btnDownload") + .addEventListener("click", downloadMusic); + document + .getElementById("btnClear") + .addEventListener("click", clearPlaylist); + musicIdInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") searchMusic(); + }); + musicPlayer.addEventListener("error", () => + showToast("试听失败:该歌曲可能无版权", "error"), + ); + document + .getElementById("resourceSearch") + .addEventListener("input", (e) => { + resourceKeyword = e.target.value.trim().toLowerCase(); + applyResourceFilter(); + }); + + // ==================== Keyboard Shortcuts ==================== + document.addEventListener("keydown", (e) => { + if ( + e.target.tagName === "INPUT" || + e.target.tagName === "TEXTAREA" + ) + return; + if (e.code === "Space") { + e.preventDefault(); + if (musicPlayer.paused) musicPlayer.play(); + else musicPlayer.pause(); + } + if (e.code === "ArrowRight") + musicPlayer.currentTime = Math.min( + musicPlayer.duration || 0, + musicPlayer.currentTime + 5, + ); + if (e.code === "ArrowLeft") + musicPlayer.currentTime = Math.max( + 0, + musicPlayer.currentTime - 5, + ); + }); + + // ==================== Swipe Navigation ==================== + let touchStartX = 0, + touchStartY = 0, + touchStartTime = 0; + const pageOrder = [ + "pageSearch", + "pageResources", + "pagePlaylist", + "pageAbout", + ]; + document.addEventListener( + "touchstart", + (e) => { + touchStartX = e.touches[0].clientX; + touchStartY = e.touches[0].clientY; + touchStartTime = Date.now(); + }, + { passive: true }, + ); + document.addEventListener( + "touchend", + (e) => { + const dx = e.changedTouches[0].clientX - touchStartX; + const dy = e.changedTouches[0].clientY - touchStartY; + const dt = Date.now() - touchStartTime; + if ( + Math.abs(dx) < 60 || + Math.abs(dx) < Math.abs(dy) * 1.5 || + dt > 500 + ) + return; + const idx = pageOrder.findIndex((id) => + document + .getElementById(id) + ?.classList.contains("active"), + ); + if (idx < 0) return; + if (dx < 0 && idx < pageOrder.length - 1) + switchPage(pageOrder[idx + 1]); + else if (dx > 0 && idx > 0) switchPage(pageOrder[idx - 1]); + }, + { passive: true }, + ); + + // ==================== Init ==================== + loadResources(); + loadPlaylist(); + loadCacheInfo(); + setInterval(() => { + loadResources(); + loadPlaylist(); + loadCacheInfo(); + }, 5000);