v2.6 设置页一键刷新缺失封面
- POST /api/music/refresh-covers (requireAdmin) - 设置页封面维护卡片 + 前端事件 - 真实场景验证:清空封面后可刷回
This commit is contained in:
1 parent
f07fbbc21f
commit
02ea161134
4 files changed
+86
-1
No files matched your search
+21
-1
@@ -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>` 从最近提交恢复,比手工修快且稳。
|
||||
@@ -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">
|
||||
|
||||
@@ -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 (_) {}
|
||||
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user