v2.6 设置页一键刷新缺失封面

- POST /api/music/refresh-covers (requireAdmin)
- 设置页封面维护卡片 + 前端事件
- 真实场景验证:清空封面后可刷回
This commit is contained in:
Xiao-no-love committed 2026-09-25 22:41:45 +08:00
1 parent f07fbbc21f
commit 02ea161134
4 files changed
+86 -1

No files matched your search

+21 -1
View File
@@ -456,4 +456,24 @@ python test_enrich.py # 给 5 首补封面(已跑过,5/5 成功)
### 18.7 未做
- **播放延迟调节**(HLS_TIME 运行时热更新):老板明确暂缓。
- 用户密码/管理员密码的 UI 设置界面(现走环境变量)。
- 会话表已有 role 列,但登录失败锁定的 failMap 是内存态,重启清零(可接受)。
- 会话表已有 role 列,但登录失败锁定的 failMap 是内存态,重启清零(可接受)。
---
## 19. v2.6 一键刷新缺失封面(已完成 2026-09-25)
### 19.1 需求
设置页加「刷新缺失封面」按钮:为所有没有封面的音乐重新匹配网易云封面。
### 19.2 实现
- **后端**:`routes/music.js` 新增 `POST /api/music/refresh-covers`(`requireAdmin`)。遍历 `cover` 和 `cover_remote` **都为空**的歌曲,逐首调 `enrichSong` 重匹配,返回 `{total, success, fail, details}`。
- **HTML**:`index.html` 设置页新增「封面维护」卡片(`#btnRefreshCovers` + `#coverRefreshProgress`),位于缓存管理和运行配置之间。
- **前端**:`app.js` 绑定按钮 → confirm 确认 → POST → 显示「共 N 首,成功 X,失败 Y」→ 刷新资源列表 + 收听页封面。
### 19.3 验证
- admin 调用 → `{total:0}`(无缺失时正常);user 调用 → **403**。
- **真实场景**:手动清空 1 首歌封面 → 调接口返回 `{total:1, success:1}` → 复查无封面歌曲数归 0 ✅。
### 19.4 工具坑(重要教训)
- **powershell Select-String 行号 ≠ replace_lines 行号**(差约 2 行,因换行/编码处理不同),导致多段 replace_lines 反复错位、文件被搞乱。
- **正解**:用 node 写查找小工具(`fs.readFileSync(f,"utf8").split("\n").forEach`),行号与 replace_lines 完全一致。改前必查、改后必 `node --check`。
- 文件搞乱时:`git checkout <commit> -- <file>` 从最近提交恢复,比手工修快且稳。
+14
View File
@@ -265,6 +265,20 @@
</div>
</div>
<div class="section-card">
<div class="section-header">
<div class="section-title">
<i class="bi bi-image" style="color: var(--accent)"></i>
封面维护
</div>
<button class="btn-clear" id="btnRefreshCovers"><i class="bi bi-arrow-repeat"></i> 刷新缺失封面</button>
</div>
<div class="section-body">
<div class="cache-stat-note" id="coverRefreshNote">为所有没有封面的音乐重新匹配网易云封面(远程优先,本地兜底)</div>
<div class="import-progress" id="coverRefreshProgress"></div>
</div>
</div>
<div class="section-card">
<div class="section-header">
<div class="section-title">
+26
View File
@@ -899,6 +899,32 @@ document.getElementById("btnCleanup").addEventListener("click", async () => {
});
document.getElementById("logTail").addEventListener("change", loadLogs);
// 一键刷新缺失封面
document.getElementById("btnRefreshCovers").addEventListener("click", async () => {
const btn = document.getElementById("btnRefreshCovers");
const prog = document.getElementById("coverRefreshProgress");
if (!confirm("为所有没有封面的音乐重新匹配网易云封面?")) return;
btn.disabled = true;
prog.textContent = "正在刷新,请稍候...";
try {
const r = await post("/api/music/refresh-covers");
if (r.total === 0) {
prog.textContent = "没有缺失封面的音乐";
showToast("没有缺失封面的音乐", "success");
} else {
prog.textContent = "完成:共 " + r.total + " 首,成功 " + r.success + ",失败 " + r.fail;
showToast("封面刷新完成:成功 " + r.success + "/" + r.total, r.fail ? "error" : "success");
await loadResources();
if (typeof loadLiveCover === "function") loadLiveCover();
}
} catch (e) {
prog.textContent = "";
showToast("刷新失败: " + e.message, "error");
} finally {
btn.disabled = false;
}
});
// ==================== Init ====================
(async function init() {
try { const me = await api("/api/auth/me"); state.role = me.role || null; applyRoleUI(state.role); } catch (_) {}
+25
View File
@@ -265,4 +265,29 @@ router.post("/enrich", async (req, res) => {
}
});
/* ==================== 一键刷新缺失封面 ==================== */
router.post("/refresh-covers", require("../auth").requireAdmin, async (req, res) => {
const songs = store.getAllSongs();
const missing = songs.filter((s) => !s.cover && !s.cover_remote);
let success = 0, fail = 0;
const details = [];
for (const s of missing) {
try {
const song = await meta.enrichSong(s.file);
if (song && (song.cover || song.cover_remote)) {
success++;
details.push({ file: s.file, ok: true });
} else {
fail++;
details.push({ file: s.file, ok: false, error: "未匹配到封面" });
}
} catch (e) {
fail++;
details.push({ file: s.file, ok: false, error: e.message });
}
}
logger.info("[COVER] 刷新缺失封面: 共 " + missing.length + " 首,成功 " + success + ",失败 " + fail);
res.json({ ok: true, total: missing.length, success, fail, details });
});
module.exports = router;