WWW.YOUINFO.SITE
标签聚合 歌词

/tag/歌词

V2EX - 技术 · 2026-06-10 15:34:26+08:00 · tech

之前有 3 个 Voice 号通过 web 版每月手动互发不同语种歌词。 三月底互发时,一个近十年的号和一个十多年的号在各发送一条信息后 voice 账号被禁用了。 多次去 Google 申诉都是同样封禁不给用。去 FCC 投诉,Google 拖到最后一天回复让通过链接地址去申诉,结果还是不给解封。 为了暂时保住最后一个注册时间最长的 Voice 号,在刷了 xiaomi.eu 的 Note 12 Tubro 上安装了 Voice 应用,24 小时开启 SS 配置绕过大陆地址。 Voice 应用没有开启自启动,省电策略是后台运行超过 10 分钟后关闭。现状是不论停止运行多久只要收到信息都会第一时间推送通知。 本机上另外一个能抽风式推送通知的是智能省电策略的淘宝。其他 play 版应用即使点击打开都不会用推送通知。

V2EX - 技术 · 2026-06-10 14:34:26+08:00 · tech

之前有 3 个 Voice 号通过 web 版每月手动互发不同语种歌词。 三月底互发时,一个近十年的号和一个十多年的号在各发送一条信息后 voice 账号被禁用了。 多次去 Google 申诉都是同样封禁不给用。去 FCC 投诉,Google 拖到最后一天回复让通过链接地址去申诉,结果还是不给解封。 为了暂时保住最后一个注册时间最长的 Voice 号,在刷了 xiaomi.eu 的 Note 12 Tubro 上安装了 Voice 应用,24 小时开启 SS 配置绕过大陆地址。 Voice 应用没有开启自启动,省电策略是后台运行超过 10 分钟后关闭。现状是不论停止运行多久只要收到信息都会第一时间推送通知。 本机上另外一个能抽风式推送通知的是智能省电策略的淘宝。其他 play 版应用即使点击打开都不会用推送通知。

V2EX - 技术 · 2026-06-10 14:34:26+08:00 · tech

之前有 3 个 Voice 号通过 web 版每月手动互发不同语种歌词。 三月底互发时,一个近十年的号和一个十多年的号在各发送一条信息后 voice 账号被禁用了。 多次去 Google 申诉都是同样封禁不给用。去 FCC 投诉,Google 拖到最后一天回复让通过链接地址去申诉,结果还是不给解封。 为了暂时保住最后一个注册时间最长的 Voice 号,在刷了 xiaomi.eu 的 Note 12 Tubro 上安装了 Voice 应用,24 小时开启 SS 配置绕过大陆地址。 Voice 应用没有开启自启动,省电策略是后台运行超过 10 分钟后关闭。现状是不论停止运行多久只要收到信息都会第一时间推送通知。 本机上另外一个能抽风式推送通知的是智能省电策略的淘宝。其他 play 版应用即使点击打开都不会用推送通知。

V2EX - 技术 · 2026-06-10 13:34:26+08:00 · tech

之前有 3 个 Voice 号通过 web 版每月手动互发不同语种歌词。 三月底互发时,一个近十年的号和一个十多年的号在各发送一条信息后 voice 账号被禁用了。 多次去 Google 申诉都是同样封禁不给用。去 FCC 投诉,Google 拖到最后一天回复让通过链接地址去申诉,结果还是不给解封。 为了暂时保住最后一个注册时间最长的 Voice 号,在刷了 xiaomi.eu 的 Note 12 Tubro 上安装了 Voice 应用,24 小时开启 SS 配置绕过大陆地址。 Voice 应用没有开启自启动,省电策略是后台运行超过 10 分钟后关闭。现状是不论停止运行多久只要收到信息都会第一时间推送通知。 本机上另外一个能抽风式推送通知的是智能省电策略的淘宝。其他 play 版应用即使点击打开都不会用推送通知。

V2EX - 技术 · 2026-06-10 12:34:26+08:00 · tech

之前有 3 个 Voice 号通过 web 版每月手动互发不同语种歌词。 三月底互发时,一个近十年的号和一个十多年的号在各发送一条信息后 voice 账号被禁用了。 多次去 Google 申诉都是同样封禁不给用。去 FCC 投诉,Google 拖到最后一天回复让通过链接地址去申诉,结果还是不给解封。 为了暂时保住最后一个注册时间最长的 Voice 号,在刷了 xiaomi.eu 的 Note 12 Tubro 上安装了 Voice 应用,24 小时开启 SS 配置绕过大陆地址。 Voice 应用没有开启自启动,省电策略是后台运行超过 10 分钟后关闭。现状是不论停止运行多久只要收到信息都会第一时间推送通知。 本机上另外一个能抽风式推送通知的是智能省电策略的淘宝。其他 play 版应用即使点击打开都不会用推送通知。

LinuxDo 最新话题 · 2026-06-10 10:51:41+08:00 · tech

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>FLAC 歌词嵌入 · LRCLib</title> <style> :root { --bg: #08080f; --surface: #111118; --surface2: #181820; --border: #222230; --text: #e0e0e8; --text2: #8888a0; --accent: #a78bfa; --accent2: #7c5cfc; --green: #34d399; --gold: #fbbf24; --radius: 16px; --radius-sm: 10px; --radius-xs: 8px; } * { margin: 0; padding: 0; box-sizing: border-box; } body { background: var(--bg); font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif; min-height: 100vh; display: flex; justify-content: center; padding: 28px 16px; color: var(--text); } .container { width: 100%; max-width: 960px; display: flex; flex-direction: column; gap: 18px; } .header { text-align: center; padding: 8px 0 4px; } .header h1 { font-size: 1.7rem; font-weight: 700; letter-spacing: -0.4px; background: linear-gradient(135deg, #a78bfa, #34d399); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .header .sub { font-size: 0.82rem; color: var(--text2); margin-top: 2px; } .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 22px; } .card-label { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 1.5px; color: var(--text2); margin-bottom: 12px; font-weight: 600; } .drop-wrapper { position: relative; } .drop-wrapper input[type="file"] { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; z-index: 2; } .drop-zone { background: var(--surface2); border: 2px dashed #2a2a3e; border-radius: var(--radius); padding: 32px 20px; text-align: center; transition: all 0.2s; pointer-events: none; } .drop-wrapper.drag-over .drop-zone { border-color: var(--accent); background: #1a1a28; box-shadow: 0 0 0 6px rgba(124, 92, 252, 0.15); } .drop-wrapper:hover .drop-zone { border-color: var(--accent); background: #1a1a28; box-shadow: 0 0 0 6px rgba(124, 92, 252, 0.06); } .drop-zone .dz-icon { font-size: 2.4rem; margin-bottom: 8px; opacity: 0.8; } .drop-zone .dz-title { font-weight: 600; font-size: 0.95rem; } .drop-zone .dz-hint { font-size: 0.78rem; color: var(--text2); margin-top: 4px; } .file-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; } .chip { display: inline-flex; align-items: center; gap: 6px; background: var(--surface2); border: 1px solid var(--border); padding: 6px 12px; border-radius: 20px; font-size: 0.8rem; font-family: 'SF Mono', 'Consolas', monospace; color: #c0c0d0; } .chip .chip-tag { font-size: 0.64rem; background: #1e1e30; color: var(--accent); padding: 2px 7px; border-radius: 10px; font-weight: 500; } .chip .chip-del { cursor: pointer; color: #666; font-weight: 700; font-size: 1rem; line-height: 1; margin-left: 2px; transition: color 0.15s; } .chip .chip-del:hover { color: #f87171; } .row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } .row.mt { margin-top: 12px; } .input { flex: 1; min-width: 180px; padding: 11px 16px; border-radius: var(--radius-xs); border: 1px solid var(--border); background: var(--surface2); color: var(--text); font-size: 0.9rem; outline: none; font-family: inherit; transition: border-color 0.2s; } .input:focus { border-color: var(--accent); } .input::placeholder { color: #555; } .btn { padding: 10px 20px; border-radius: 24px; border: 1px solid var(--border); background: var(--surface2); color: var(--text); cursor: pointer; font-size: 0.85rem; font-weight: 600; transition: all 0.2s; white-space: nowrap; display: inline-flex; align-items: center; gap: 5px; font-family: inherit; letter-spacing: 0.2px; } .btn:hover { background: #222238; border-color: #444; } .btn-primary { background: var(--accent2); border-color: var(--accent2); color: #fff; box-shadow: 0 4px 18px rgba(124, 92, 252, 0.25); } .btn-primary:hover { background: #8f6fff; border-color: #8f6fff; box-shadow: 0 6px 24px rgba(124, 92, 252, 0.35); } .btn-sm { padding: 6px 14px; font-size: 0.76rem; border-radius: 18px; } .btn-xs { padding: 4px 10px; font-size: 0.7rem; border-radius: 14px; } .btn:disabled { opacity: 0.35; cursor: not-allowed; pointer-events: none; } .hint { font-size: 0.72rem; color: var(--text2); margin-top: 6px; font-style: italic; } .results-box { display: none; max-height: 280px; overflow-y: auto; margin-top: 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface2); } .results-box.open { display: block; } .result-row { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; cursor: pointer; transition: background 0.12s; gap: 10px; flex-wrap: wrap; } .result-row:hover { background: #1e1e2c; } .result-row+.result-row { border-top: 1px solid rgba(255, 255, 255, 0.03); } .result-row.selected { background: #1a1830; border-left: 3px solid var(--accent); } .result-info { flex: 1; min-width: 0; } .result-info .rtrack { font-weight: 600; font-size: 0.88rem; } .result-info .rartist { font-size: 0.76rem; color: var(--text2); } .result-meta { font-size: 0.7rem; color: #555; white-space: nowrap; } .empty { text-align: center; color: var(--text2); padding: 28px; font-size: 0.85rem; } .spinner-wrap { text-align: center; padding: 28px; color: var(--text2); display: flex; align-items: center; justify-content: center; gap: 8px; } .spinner { width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.7s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .lyrics-panel { display: none; margin-top: 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; } .lyrics-panel.open { display: block; } .lyrics-top { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; padding: 12px 16px; background: #14141e; border-bottom: 1px solid var(--border); } .lyrics-top .ltitle { font-weight: 700; font-size: 1rem; } .lyrics-top .lartist { color: var(--accent); font-size: 0.82rem; } .lyrics-top .lalbum { color: var(--text2); font-size: 0.76rem; } .tabs { display: flex; gap: 3px; flex-wrap: wrap; } .tab { padding: 5px 12px; border-radius: 16px; border: 1px solid var(--border); background: transparent; color: var(--text2); cursor: pointer; font-size: 0.72rem; font-weight: 500; transition: 0.2s; font-family: inherit; } .tab.on { background: var(--accent2); border-color: var(--accent2); color: #fff; } .lyrics-content { padding: 16px; max-height: 340px; overflow-y: auto; background: #0c0c16; font-family: 'SF Mono', 'Fira Code', 'Consolas', 'PingFang SC', monospace; font-size: 0.8rem; line-height: 1.75; white-space: pre-wrap; color: #c0c0d0; } .lyrics-content .hl-tag { color: var(--accent); font-weight: 600; } .lyrics-content .hl-idx { color: #666; } .lyrics-content .hl-time { color: var(--green); } .lyrics-content .hl-section { color: var(--gold); } .lyrics-content .hl-style { color: #60a5fa; } .check-row { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; padding: 10px 0; font-size: 0.76rem; color: var(--text2); } .check-row label { display: flex; align-items: center; gap: 5px; cursor: pointer; user-select: none; } .check-row input[type="checkbox"] { accent-color: var(--accent2); width: 15px; height: 15px; cursor: pointer; } .action-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; padding-top: 12px; border-top: 1px solid var(--border); } .tag-chips { display: flex; gap: 4px; flex-wrap: wrap; } .tag-chip { padding: 4px 10px; border-radius: 14px; border: 1px solid var(--border); background: transparent; color: var(--text2); cursor: pointer; font-size: 0.68rem; font-weight: 500; transition: 0.2s; user-select: none; font-family: inherit; } .tag-chip.on { background: var(--accent2); border-color: var(--accent2); color: #fff; } .toast { position: fixed; top: 16px; left: 50%; transform: translateX(-50%); padding: 10px 22px; border-radius: 24px; font-weight: 600; font-size: 0.82rem; z-index: 999; opacity: 0; pointer-events: none; transition: opacity 0.3s; letter-spacing: 0.2px; } .toast.show { opacity: 1; } .toast.ok { background: #065f46; color: #d1fae5; box-shadow: 0 8px 28px rgba(5, 150, 105, 0.3); } .toast.err { background: #7f1d1d; color: #fecaca; box-shadow: 0 8px 28px rgba(220, 38, 38, 0.3); } .footer-note { text-align: center; font-size: 0.7rem; color: #555; } ::-webkit-scrollbar { width: 4px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: #333; border-radius: 2px; } @media (max-width: 640px) { .row { flex-direction: column; align-items: stretch; } .btn { justify-content: center; } .action-row { flex-direction: column; align-items: stretch; } } </style> </head> <body> <div class="container"> <div class="header"> <h1>🎵 FLAC 歌词嵌入工具</h1> <div class="sub">LRCLib 歌词搜索 · Vorbis Comment 标签写入 · 元数据覆盖</div> </div> <div class="card"> <div class="card-label">📂 选择 FLAC 文件</div> <div class="drop-wrapper" id="dropWrapper"> <input type="file" id="fileInput" accept=".flac,audio/flac" multiple> <div class="drop-zone"> <div class="dz-icon">🎶</div> <div class="dz-title">点击选择或拖拽 FLAC 文件</div> <div class="dz-hint">支持批量 · 自动读取 TITLE / ARTIST / ALBUM 标签</div> </div> </div> <div class="file-chips" id="fileChips"></div> <div class="row mt"> <button class="btn btn-sm" id="clearFilesBtn" disabled>🗑 清空</button> <span style="font-size:0.76rem;color:var(--text2);" id="fileStatus">等待添加 FLAC…</span> </div> </div> <div class="card"> <div class="card-label">🔍 搜索歌词</div> <div class="row"> <input class="input" id="searchInput" placeholder="歌曲名 或「歌曲名 - 歌手名」" autocomplete="off"> <button class="btn btn-primary" id="searchBtn">搜索</button> <button class="btn btn-sm" id="directBtn">🎯 精确获取</button> </div> <div class="hint" id="autoFillHint"></div> <div class="results-box" id="resultsBox"> <div id="resultsList"></div> </div> </div> <div class="card" id="lyricsCard" style="display:none;"> <div class="lyrics-panel open" id="lyricsPreview"> <div class="lyrics-top"> <div> <span class="ltitle" id="songName">—</span> <span style="margin:0 5px;color:#555;">·</span> <span class="lartist" id="songArtist">—</span> <span style="margin:0 5px;color:#555;">·</span> <span class="lalbum" id="songAlbum">—</span> </div> <div class="tabs"> <button class="tab on" data-fmt="lrc">LRC</button> <button class="tab" data-fmt="srt">SRT</button> <button class="tab" data-fmt="ass">ASS</button> <button class="tab" data-fmt="vtt">VTT</button> </div> </div> <div class="lyrics-content" id="lyricsContent"></div> </div> <div class="check-row"> <span>🔧 写入时覆盖:</span> <label><input type="checkbox" id="ovTitle" checked> TITLE</label> <label><input type="checkbox" id="ovArtist" checked> ARTIST</label> <label><input type="checkbox" id="ovAlbum" checked> ALBUM</label> <span style="color:#555;font-size:0.68rem;">用 API 返回信息覆盖 FLAC 标签</span> </div> <div class="action-row"> <span style="font-size:0.72rem;color:var(--text2);font-weight:600;">歌词标签</span> <div class="tag-chips" id="tagChips"> <span class="tag-chip on" data-tag="LYRICS">LYRICS</span> <span class="tag-chip" data-tag="UNSYNCEDLYRICS">UNSYNCEDLYRICS</span> <span class="tag-chip" data-tag="LYRICS_LRC">LYRICS_LRC</span> <span class="tag-chip" data-tag="LYRICS_SRT">LYRICS_SRT</span> <span class="tag-chip" data-tag="LYRICS_ASS">LYRICS_ASS</span> <span class="tag-chip" data-tag="LYRICS_VTT">LYRICS_VTT</span> </div> <span style="flex:1;"></span> <button class="btn btn-primary" id="embedBtn" disabled>💾 写入并下载</button> </div> </div> <div class="footer-note">浏览器安全限制:通过下载生成新文件,原文件不被修改</div> </div> <div class="toast" id="toast"></div> <script> (function() { var DW = document.getElementById('dropWrapper'); var FI = document.getElementById('fileInput'); var FC = document.getElementById('fileChips'); var CFB = document.getElementById('clearFilesBtn'); var FST = document.getElementById('fileStatus'); var SI = document.getElementById('searchInput'); var SB = document.getElementById('searchBtn'); var DB = document.getElementById('directBtn'); var AFH = document.getElementById('autoFillHint'); var RB = document.getElementById('resultsBox'); var RL = document.getElementById('resultsList'); var LC = document.getElementById('lyricsCard'); var LCT = document.getElementById('lyricsContent'); var SN = document.getElementById('songName'); var SA = document.getElementById('songArtist'); var SAL = document.getElementById('songAlbum'); var EB = document.getElementById('embedBtn'); var OVT = document.getElementById('ovTitle'); var OVA = document.getElementById('ovArtist'); var OVAL = document.getElementById('ovAlbum'); var TO = document.getElementById('toast'); var TABS = document.querySelectorAll('.tab'); var TCHIPS = document.querySelectorAll('.tag-chip'); var files = []; var song = null; var fmt = 'lrc'; var tag = 'LYRICS'; var results = []; var selectedIdx = -1; var tt; function toast(m, e) { clearTimeout(tt); TO.textContent = m; TO.className = 'toast show ' + (e ? 'err' : 'ok'); tt = setTimeout(function() { TO.className = 'toast'; }, 2200); } function esc(s) { var d = document.createElement('div'); d.appendChild(document.createTextNode(s)); return d.innerHTML; } function p2(n) { return String(n).padStart(2, '0'); } function p3(n) { return String(n).padStart(3, '0'); } function readMeta(file, cb) { var r = new FileReader(); r.onload = function(e) { try { var b = e.target.result; var v = new DataView(b); if (b.byteLength < 4) return cb({ t: null, a: null, c: null, al: null }); if (String.fromCharCode(v.getUint8(0), v.getUint8(1), v.getUint8(2), v.getUint8(3)) !== 'fLaC') return cb({ t: null, a: null, c: null, al: null }); var o = 4, lb = false, t = null, a = null, c = null, al = null; while (o < b.byteLength && !lb) { if (o + 4 > b.byteLength) break; var h = v.getUint8(o); lb = (h & 0x80) !== 0; var bt = h & 0x7F; var bs = (v.getUint8(o + 1) << 16) | (v.getUint8(o + 2) << 8) | v.getUint8(o + 3); o += 4; if (bt === 4 && o + bs <= b.byteLength) { var bl = new Uint8Array(b, o, bs); var dec = new TextDecoder('utf-8'); if (bl.length >= 4) { var vl = bl[0] | (bl[1] << 8) | (bl[2] << 16) | (bl[3] << 24); var p = 4 + vl; if (p + 4 <= bl.length) { var nc = bl[p] | (bl[p + 1] << 8) | (bl[p + 2] << 16) | (bl[p + 3] << 24); p += 4; for (var i = 0; i < nc && p + 4 <= bl.length; i++) { var cl = bl[p] | (bl[p + 1] << 8) | (bl[p + 2] << 16) | (bl[p + 3] << 24); p += 4; if (p + cl > bl.length) break; var cs = dec.decode(bl.slice(p, p + cl)); p += cl; var ei = cs.indexOf('='); if (ei > 0) { var k = cs.substring(0, ei).toUpperCase().trim(); var val = cs.substring(ei + 1).trim(); if (k === 'TITLE' && !t) t = val; if (k === 'ARTIST' && !a) a = val; if (k === 'COMPOSER' && !c) c = val; if (k === 'ALBUM' && !al) al = val; } } } } break; } o += bs; } cb({ t: t, a: a, c: c, al: al }); } catch (x) { cb({ t: null, a: null, c: null, al: null }); } }; r.onerror = function() { cb({ t: null, a: null, c: null, al: null }); }; r.readAsArrayBuffer(file); } function autoFill() { if (files.length === 0) { AFH.textContent = ''; return; } var t = files[0].t || ''; var a = files[0].a || files[0].c || ''; if (t) { var ft = t; if (a) ft += ' - ' + a; SI.value = ft; AFH.textContent = '📋 已从 FLAC 标签自动填充:' + ft; } else { AFH.textContent = '⚠️ 未找到 TITLE 标签,请手动输入'; } } function updateFiles() { FC.innerHTML = ''; if (files.length === 0) { CFB.disabled = true; FST.textContent = '等待添加 FLAC…'; EB.disabled = true; AFH.textContent = ''; } else { CFB.disabled = false; FST.textContent = files.length + ' 个 FLAC 文件'; if (song) EB.disabled = false; for (var i = 0; i < files.length; i++) { var f = files[i]; var el = document.createElement('span'); el.className = 'chip'; var h = '🎵 ' + esc(f.file.name); if (f.t) h += ' <span class="chip-tag">' + esc(f.t) + '</span>'; h += ' <span class="chip-del" data-idx="' + i + '">×</span>'; el.innerHTML = h; FC.appendChild(el); } var dels = FC.querySelectorAll('.chip-del'); for (var d = 0; d < dels.length; d++) { dels[d].onclick = function(e) { e.stopPropagation(); var idx = parseInt(this.getAttribute('data-idx'), 10); files.splice(idx, 1); updateFiles(); autoFill(); }; } } } function addFiles(fileList) { var only = []; for (var i = 0; i < fileList.length; i++) { if (!fileList[i].name.toLowerCase().endsWith('.flac')) continue; var dup = false; for (var j = 0; j < files.length; j++) { if (files[j].file.name === fileList[i].name && files[j].file.size === fileList[i].size && files[j].file.lastModified === fileList[i].lastModified) { dup = true; break; } } if (!dup) only.push(fileList[i]); } if (only.length === 0) { if (fileList.length > 0) toast('请选择 .flac 文件', true); return; } FST.textContent = '🔍 读取元数据…'; var done = 0; var total = only.length; function handleOne(f, meta) { files.push({ file: f, t: meta.t, a: meta.a, c: meta.c, al: meta.al }); done++; if (done >= total) { updateFiles(); autoFill(); toast('✅ 已添加 ' + total + ' 个 FLAC 文件'); if (files.length > 0 && SI.value.trim()) doSearch(SI.value); } } for (var k = 0; k < only.length; k++) { (function(fileRef) { readMeta(fileRef, function(meta) { handleOne(fileRef, meta); }); })(only[k]); } } function handleFileSelect(fileList) { if (fileList && fileList.length) { addFiles(fileList); } } FI.addEventListener('change', function(e) { handleFileSelect(e.target.files); FI.value = ''; }); DW.addEventListener('dragover', function(e) { e.preventDefault(); e.stopPropagation(); DW.classList.add('drag-over'); }); DW.addEventListener('dragleave', function(e) { e.preventDefault(); e.stopPropagation(); DW.classList.remove('drag-over'); }); DW.addEventListener('drop', function(e) { e.preventDefault(); e.stopPropagation(); DW.classList.remove('drag-over'); if (e.dataTransfer.files && e.dataTransfer.files.length) { handleFileSelect(e.dataTransfer.files); } }); CFB.addEventListener('click', function() { files = []; updateFiles(); autoFill(); toast('🧹 已清空'); }); function apiSearch(q) { return fetch('https://lrclib.net/api/search?q=' + encodeURIComponent(q)).then(function(r) { if (!r.ok) throw new Error('搜索失败'); return r.json(); }); } function apiGet(t, a) { return fetch('https://lrclib.net/api/get?track_name=' + encodeURIComponent(t) + '&artist_name=' + encodeURIComponent(a)).then(function(r) { if (!r.ok) throw new Error('获取失败'); return r.text(); }).then(function(t) { if (!t) throw new Error('未找到'); return JSON.parse(t); }); } function parseLrc(sl) { var items = []; if (!sl) return items; var re = /\[(\d{2}):(\d{2})\.(\d{2,3})\]\s*(.*)/g, m; while ((m = re.exec(sl)) !== null) { var ms = m[3].length === 2 ? parseInt(m[3], 10) * 10 : parseInt(m[3], 10); items.push({ tm: parseInt(m[1]) * 60000 + parseInt(m[2]) * 1000 + ms, tx: (m[4] || '').trim() }); } return items; } function msLrc(ms) { var mn = Math.floor(ms / 60000); return '[' + p2(mn) + ':' + p2(Math.floor((ms % 60000) / 1000)) + '.' + p2(Math.floor((ms % 1000) / 10)) + ']'; } function msSrt(ms) { var h = Math.floor(ms / 3600000); return p2(h) + ':' + p2(Math.floor((ms % 3600000) / 60000)) + ':' + p2(Math.floor((ms % 60000) / 1000)) + ',' + p3(ms % 1000); } function msAss(ms) { var h = Math.floor(ms / 3600000); return h + ':' + p2(Math.floor((ms % 3600000) / 60000)) + ':' + p2(Math.floor((ms % 60000) / 1000)) + '.' + p2(Math.floor((ms % 1000) / 10)); } function msVtt(ms) { var h = Math.floor(ms / 3600000); return p2(h) + ':' + p2(Math.floor((ms % 3600000) / 60000)) + ':' + p2(Math.floor((ms % 60000) / 1000)) + '.' + p3(ms % 1000); } function genLRC(s) { var it = parseLrc(s.syncedLyrics); var l = []; l.push('[ti:' + (s.trackName || '') + ']'); l.push('[ar:' + (s.artistName || '') + ']'); if (s.albumName) l.push('[al:' + s.albumName + ']'); if (s.duration) { var m = Math.floor(s.duration / 60); l.push('[length:' + p2(m) + ':' + p2(Math.floor(s.duration % 60)) + ']'); } l.push(''); if (it.length) { for (var i = 0; i < it.length; i++) l.push(msLrc(it[i].tm) + (it[i].tx || '♪')); } else if ( s.plainLyrics) { var pl = s.plainLyrics.split('\n'); for (var j = 0; j < pl.length; j++) l.push(pl[j] .trim()); } return l.join('\n'); } function genSRT(s) { var it = parseLrc(s.syncedLyrics); if (!it.length) return s.plainLyrics ? '1\n00:00:00,000 --> 00:03:00,000\n' + s.plainLyrics.trim() + '\n' : ''; var l = []; for (var i = 0; i < it.length; i++) { var nx = it[i + 1], em = nx ? nx.tm : it[i].tm + 3000; l.push(String(i + 1)); l.push(msSrt(it[i].tm) + ' --> ' + msSrt(em)); l.push(it[i].tx || '♪'); l.push(''); } return l.join('\n').trim(); } function genASS(s) { var it = parseLrc(s.syncedLyrics); var l = []; l.push('[Script Info]'); l.push('Title: ' + (s.trackName || 'Unknown')); l.push('Original Script: ' + (s.artistName || 'Unknown')); l.push('ScriptType: v4.00+'); l.push('Collisions: Normal'); l.push('PlayDepth: 0'); l.push(''); l.push('[V4+ Styles]'); l.push( 'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding'); l.push( 'Style: Default,Microsoft YaHei,36,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,1,2,1,2,30,30,30,1'); l.push(''); l.push('[Events]'); l.push('Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text'); if (it.length) { for (var i = 0; i < it.length; i++) { var nx = it[i + 1], em = nx ? nx.tm : it[i].tm + 3000; l.push('Dialogue: 0,' + msAss(it[i].tm) + ',' + msAss(em) + ',Default,,0,0,0,,' + (it[i].tx || '♪').replace(/\n/g, '\\N')); } } else if (s.plainLyrics) { var pl = s.plainLyrics.split('\n').filter(function(x) { return x.trim(); }); var dur = (s.duration || 180) * 1000; var each = Math.floor(dur / Math.max(pl.length, 1)); for (var j = 0; j < pl.length; j++) l.push('Dialogue: 0,' + msAss(j * each) + ',' + msAss(j * each + each) + ',Default,,0,0,0,,' + pl[j].trim().replace(/\n/g, '\\N')); } return l.join('\n'); } function genVTT(s) { var it = parseLrc(s.syncedLyrics); var l = []; l.push('WEBVTT'); l.push(''); if (it.length) { for (var i = 0; i < it.length; i++) { var nx = it[i + 1], em = nx ? nx.tm : it[i].tm + 3000; l.push(msVtt(it[i].tm) + ' --> ' + msVtt(em)); l.push(it[i].tx || '♪'); l.push(''); } } else if (s.plainLyrics) { var pl = s.plainLyrics.split('\n').filter(function(x) { return x.trim(); }); var dur = (s.duration || 180) * 1000; var each = Math.floor(dur / Math.max(pl.length, 1)); for (var j = 0; j < pl.length; j++) { l.push(msVtt(j * each) + ' --> ' + msVtt(j * each + each)); l.push(pl[j].trim()); l.push(''); } } return l.join('\n').trim(); } function getFmt(f, s) { if (!s) return ''; if (f === 'lrc') return genLRC(s); if (f === 'srt') return genSRT(s); if (f === 'ass') return genASS(s); if (f === 'vtt') return genVTT(s); return ''; } function hlHtml(f, s) { var raw = getFmt(f, s); if (!raw) return '<span style="color:#555;">暂无内容</span>'; var e = esc(raw); if (f === 'lrc') { e = e.replace(/^(\[ti:.*\]|\[ar:.*\]|\[al:.*\]|\[length:.*\])$/gm, '<span class="hl-tag">$1</span>'); e = e.replace(/^(\[\d{2}:\d{2}\.\d{2}\])/gm, '<span class="hl-tag">$1</span>'); } else if (f === 'srt') { e = e.replace(/^(\d+)$/gm, '<span class="hl-idx">$1</span>'); e = e.replace(/^(\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3})$/gm, '<span class="hl-time">$1</span>'); } else if (f === 'ass') { e = e.replace(/^(\[.*\])$/gm, '<span class="hl-section">$1</span>'); e = e.replace(/^(Style:.*)$/gm, '<span class="hl-style">$1</span>'); e = e.replace(/^(Format:.*)$/gm, '<span class="hl-style">$1</span>'); } else if (f === 'vtt') { e = e .replace(/^(WEBVTT)$/gm, '<span class="hl-section">$1</span>'); e = e.replace(/^(\d{2}:\d{2}:\d{2}\.\d{3} --> \d{2}:\d{2}:\d{2}\.\d{3})$/gm, '<span class="hl-time">$1</span>'); } return e; } function renderSong(s) { song = s; SN.textContent = s.trackName || '未知'; SA.textContent = s.artistName || '未知'; SAL.textContent = s.albumName ? '💿 ' + s.albumName : ''; LC.style.display = 'block'; if (files.length > 0) EB.disabled = false; updateLyrics(); LC.scrollIntoView({ behavior: 'smooth', block: 'center' }); } function updateLyrics() { LCT.innerHTML = hlHtml(fmt, song); } for (var t = 0; t < TABS.length; t++) { TABS[t].addEventListener('click', function() { for (var i = 0; i < TABS.length; i++) TABS[i].classList.remove('on'); this.classList.add('on'); fmt = this.getAttribute('data-fmt'); updateLyrics(); }); } for (var c = 0; c < TCHIPS.length; c++) { TCHIPS[c].addEventListener('click', function() { for (var i = 0; i < TCHIPS.length; i++) TCHIPS[i].classList.remove('on'); this.classList.add('on'); tag = this.getAttribute('data-tag'); }); } function doSearch(q) { if (!q || !q.trim()) { toast('请输入关键词', true); return; } results = []; selectedIdx = -1; RL.innerHTML = '<div class="spinner-wrap"><span class="spinner"></span>搜索中…</div>'; RB.classList.add('open'); apiSearch(q.trim()).then(function(rs) { results = rs; if (!rs.length) { RL.innerHTML = '<div class="empty">😕 未找到匹配的歌曲</div>'; return; } var h = ''; for (var i = 0; i < rs.length; i++) { var r = rs[i]; h += '<div class="result-row" data-idx="' + i + '">'; h += '<div class="result-info"><div class="rtrack">' + esc(r.trackName || r.name || '未知') + '</div><div class="rartist">' + esc(r.artistName || '未知') + '</div></div>'; h += '<div class="result-meta">' + fdur(r.duration) + '</div>'; h += '<button class="btn btn-xs pick-btn" data-idx="' + i + '">选择</button>'; h += '</div>'; } RL.innerHTML = h; bindResults(); }).catch(function(err) { RL.innerHTML = '<div class="empty">❌ ' + esc(err.message) + '</div>'; }); } function bindResults() { var rows = RL.querySelectorAll('.result-row'); for (var j = 0; j < rows.length; j++) { (function(idx) { rows[j].addEventListener('click', function(e) { if (e.target.closest('.pick-btn')) return; loadResult(idx); }); })(j); } var btns = RL.querySelectorAll('.pick-btn'); for (var k = 0; k < btns.length; k++) { (function(idx) { btns[k].addEventListener('click', function(e) { e.stopPropagation(); loadResult(idx); }); })(k); } } function loadResult(idx) { var s = results[idx]; if (!s) return; selectedIdx = idx; var tn = s.trackName || s.name; var an = s.artistName || ''; var rows = RL.querySelectorAll('.result-row'); for (var i = 0; i < rows.length; i++) { rows[i].classList.remove('selected'); rows[i].style.opacity = '0.4'; } var tgt = RL.querySelector('[data-idx="' + idx + '"]'); if (tgt) { tgt.classList.add('selected'); tgt.style.opacity = '1'; } apiGet(tn, an).then(function(d) { renderSong(d); for (var j = 0; j < rows.length; j++) rows[j].style.opacity = '1'; }).catch(function(err) { toast('❌ ' + err.message, true); for (var j = 0; j < rows.length; j++) rows[j].style.opacity = '1'; selectedIdx = -1; }); } function fdur(s) { if (!s && s !== 0) return ''; var m = Math.floor(s / 60); return m + ':' + p2(Math.floor(s % 60)); } SB.addEventListener('click', function() { doSearch(SI.value); }); SI.addEventListener('keydown', function(e) { if (e.key === 'Enter') doSearch(SI.value); }); DB.addEventListener('click', function() { var q = SI.value.trim(); if (!q) { toast('请输入「歌曲名 - 歌手名」', true); return; } var seps = [' - ', '-', ' – ', '–', ' | ', '|', ':', ':']; var tk = '', ar = ''; for (var i = 0; i < seps.length; i++) { if (q.indexOf(seps[i]) !== -1) { var pts = q.split(seps[i]); tk = pts[0].trim(); ar = pts.slice(1).join(seps[i]).trim(); break; } } if (!tk) { tk = prompt('歌曲名:', q); if (!tk) return; ar = prompt('歌手名(可选):', '') || ''; } results = []; selectedIdx = -1; RL.innerHTML = '<div class="spinner-wrap"><span class="spinner"></span>获取中…</div>'; RB.classList.add('open'); apiGet(tk, ar).then(function(d) { results = [d]; selectedIdx = 0; RL.innerHTML = '<div class="result-row selected" data-idx="0" style="opacity:1;"><div class="result-info"><div class="rtrack">' + esc(d.trackName || d.name || '未知') + '</div><div class="rartist">' + esc(d.artistName || '未知') + '</div></div><div class="result-meta">' + fdur(d.duration) + '</div></div>'; renderSong(d); }).catch(function(err) { RL.innerHTML = '<div class="empty">❌ ' + esc(err.message) + '</div>'; toast('❌ ' + err.message, true); }); }); function readBlocks(ab) { var v = new DataView(ab); var b = ab; if (b.byteLength < 4) throw new Error('太小'); if (String.fromCharCode(v.getUint8(0), v.getUint8(1), v.getUint8(2), v.getUint8(3)) !== 'fLaC') throw new Error( '非FLAC'); var bl = []; var o = 4, lb = false; while (o < b.byteLength && !lb) { if (o + 4 > b.byteLength) break; var h = v.getUint8(o); lb = (h & 0x80) !== 0; var bt = h & 0x7F; var bs = (v.getUint8(o + 1) << 16) | (v.getUint8(o + 2) << 8) | v.getUint8(o + 3); var st = o; o += 4; bl.push({ type: bt, last: lb, ho: st, dto: o, size: bs }); o += bs; } return { blocks: bl, buf: b }; } function parseVC(bi, buf) { var bl = new Uint8Array(buf, bi.dto, bi.size); var dec = new TextDecoder('utf-8'); if (bl.length < 4) return { cmts: [] }; var vl = bl[0] | (bl[1] << 8) | (bl[2] << 16) | (bl[3] << 24); var p = 4 + vl; if (p + 4 > bl.length) return { cmts: [] }; var nc = bl[p] | (bl[p + 1] << 8) | (bl[p + 2] << 16) | (bl[p + 3] << 24); p += 4; var cmts = []; for (var i = 0; i < nc; i++) { if (p + 4 > bl.length) break; var cl = bl[p] | (bl[p + 1] << 8) | (bl[p + 2] << 16) | (bl[p + 3] << 24); p += 4; if (p + cl > bl.length) break; cmts.push(dec.decode(bl.slice(p, p + cl))); p += cl; } return { cmts: cmts }; } function buildVC(cmts, isLast) { var enc = new TextEncoder(); var vb = new Uint8Array(4); var cb = new Uint8Array(4); cb[0] = cmts.length & 0xFF; cb[1] = (cmts.length >> 8) & 0xFF; cb[2] = (cmts.length >> 16) & 0xFF; cb[3] = (cmts.length >> 24) & 0xFF; var arrs = []; for (var i = 0; i < cmts.length; i++) { var ed = enc.encode(cmts[i]); var lb = new Uint8Array(4); lb[0] = ed.length & 0xFF; lb[1] = (ed.length >> 8) & 0xFF; lb[2] = (ed.length >> 16) & 0xFF; lb[3] = (ed.length >> 24) & 0xFF; arrs.push(lb); arrs.push(ed); } var tp = 8; for (var j = 0; j < arrs.length; j++) tp += arrs[j].length; var bh = new Uint8Array(4); bh[0] = 4; if (isLast) bh[0] |= 0x80; bh[1] = (tp >> 16) & 0xFF; bh[2] = (tp >> 8) & 0xFF; bh[3] = tp & 0xFF; var res = new Uint8Array(4 + tp); res.set(bh, 0); var off = 4; res.set(vb, off); off += 4; res.set(cb, off); off += 4; for (var k = 0; k < arrs.length; k++) { res.set(arrs[k], off); off += arrs[k].length; } return res; } function embed(file, lc, tn, sm, ov, cb) { var r = new FileReader(); r.onload = function(e) { try { var info = readBlocks(e.target.result); var bl = info.blocks; var buf = info.buf; var vi = -1; for (var i = 0; i < bl.length; i++) { if (bl[i].type === 4) { vi = i; break; } } var nc = []; var tu = tn.toUpperCase(); var pk = [tu]; if (ov.title) pk.push('TITLE'); if (ov.artist) pk.push('ARTIST'); if (ov.album) pk.push('ALBUM'); if (vi >= 0) { var parsed = parseVC(bl[vi], buf); for (var j = 0; j < parsed.cmts.length; j++) { var eq = parsed.cmts[j].indexOf('='); if (eq > 0) { var key = parsed.cmts[j].substring(0, eq).toUpperCase().trim(); if (pk.indexOf( key) === -1) nc.push(parsed.cmts[j]); } else nc.push(parsed.cmts[j]); } } nc.push(tu + '=' + lc); if (ov.title && sm.trackName) nc.push('TITLE=' + sm.trackName); if (ov.artist && sm.artistName) nc.push('ARTIST=' + sm.artistName); if (ov.album && sm.albumName) nc.push('ALBUM=' + sm.albumName); var isLast = (vi >= 0) ? bl[vi].last : false; var nb = buildVC(nc, isLast); var parts = []; if (vi >= 0) { parts.push(new Uint8Array(buf, 0, bl[vi].ho)); parts.push(nb); parts.push(new Uint8Array(buf, bl[vi].dto + bl[vi].size)); } else { var si = -1; for (var k = 0; k < bl.length; k++) { if (bl[k].type === 0) { si = k; break; } } if (si >= 0) { var sho = bl[si].ho; var sa = bl[si].dto + bl[si].size; parts.push(new Uint8Array(buf, 0, sho)); var sh = new Uint8Array(buf, sho, 4); sh[0] = sh[0] & 0x7F; parts.push(sh); parts.push(new Uint8Array(buf, bl[si].dto, bl[si].size)); parts.push(nb); parts.push(new Uint8Array(buf, sa)); } else { parts.push(new Uint8Array(buf, 0, 4)); parts.push(nb); parts.push(new Uint8Array(buf, 4)); } } var tl = 0; for (var p = 0; p < parts.length; p++) tl += parts[p].length; var result = new Uint8Array(tl); var off = 0; for (var q = 0; q < parts.length; q++) { result.set(parts[q], off); off += parts[q].length; } cb(null, new Blob([result], { type: 'audio/flac' })); } catch (x) { cb(x); } }; r.onerror = function() { cb(new Error('读取失败')); }; r.readAsArrayBuffer(file); } EB.addEventListener('click', function() { if (!files.length) { toast('请先添加 FLAC 文件', true); return; } if (!song) { toast('请先选择歌词', true); return; } var lc = getFmt(fmt, song); if (!lc.trim()) { toast('当前格式无内容', true); return; } var ov = { title: OVT.checked, artist: OVA.checked, album: OVAL.checked }; EB.disabled = true; CFB.disabled = true; var extra = []; if (ov.title) extra.push('TITLE'); if (ov.artist) extra.push('ARTIST'); if (ov.album) extra.push('ALBUM'); var em = extra.length ? ' + 覆盖 ' + extra.join('/') : ''; toast('⏳ 正在写入 ' + files.length + ' 个文件' + em + '…'); var ok = 0, fl = 0; function next(idx) { if (idx >= files.length) { EB.disabled = false; CFB.disabled = false; if (fl === 0) toast('🎉 成功!已下载 ' + ok + ' 个 FLAC 文件' + em); else toast('⚠️ ' + ok + ' 成功, ' + fl + ' 失败', true); return; } embed(files[idx].file, lc, tag, song, ov, function(err, blob) { if (err) { fl++; console.error(err); } else { var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = files[idx].file.name; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); ok++; } setTimeout(function() { next(idx + 1); }, 200); }); } next(0); }); updateFiles(); SI.focus(); })(); </script> </body> </html> 歌词搜索、标签写入和元数据覆盖一次性搞定 2 个帖子 - 2 位参与者 阅读完整话题

V2EX - 技术 · 2026-06-10 09:34:06+08:00 · tech

之前有 3 个 Voice 号通过 web 版每月手动互发不同语种歌词。 三月底互发时,一个近十年的号和一个十多年的号在各发送一条信息后 voice 账号被禁用了。 多次去 Google 申诉都是同样封禁不给用。去 FCC 投诉,Google 拖到最后一天回复让通过链接地址去申诉,结果还是不给解封。 为了暂时保住最后一个注册时间最长的 Voice 号,在刷了 xiaomi.eu 的 Note 12 Tubro 上安装了 Voice 应用,24 小时开启 SS 配置绕过大陆地址。 Voice 应用没有开启自启动,省电策略是后台运行超过 10 分钟后关闭。现状是不论停止运行多久只要收到信息都会第一时间推送通知。 本机上另外一个能抽风式推送通知的是智能省电策略的淘宝。其他 play 版应用即使点击打开都不会用推送通知。

V2EX - 技术 · 2026-06-10 09:34:06+08:00 · tech

之前有 3 个 Voice 号通过 web 版每月手动互发不同语种歌词。 三月底互发时,一个近十年的号和一个十多年的号在各发送一条信息后 voice 账号被禁用了。 多次去 Google 申诉都是同样封禁不给用。去 FCC 投诉,Google 拖到最后一天回复让通过链接地址去申诉,结果还是不给解封。 为了暂时保住最后一个注册时间最长的 Voice 号,在刷了 xiaomi.eu 的 Note 12 Tubro 上安装了 Voice 应用,24 小时开启 SS 配置绕过大陆地址。 Voice 应用没有开启自启动,省电策略是后台运行超过 10 分钟后关闭。现状是不论停止运行多久只要收到信息都会第一时间推送通知。 本机上另外一个能抽风式推送通知的是智能省电策略的淘宝。其他 play 版应用即使点击打开都不会用推送通知。

V2EX - 技术 · 2026-06-10 09:34:06+08:00 · tech

之前有 3 个 Voice 号通过 web 版每月手动互发不同语种歌词。 三月底互发时,一个近十年的号和一个十多年的号在各发送一条信息后 voice 账号被禁用了。 多次去 Google 申诉都是同样封禁不给用。去 FCC 投诉,Google 拖到最后一天回复让通过链接地址去申诉,结果还是不给解封。 为了暂时保住最后一个注册时间最长的 Voice 号,在刷了 xiaomi.eu 的 Note 12 Tubro 上安装了 Voice 应用,24 小时开启 SS 配置绕过大陆地址。 Voice 应用没有开启自启动,省电策略是后台运行超过 10 分钟后关闭。现状是不论停止运行多久只要收到信息都会第一时间推送通知。 本机上另外一个能抽风式推送通知的是智能省电策略的淘宝。其他 play 版应用即使点击打开都不会用推送通知。

V2EX - 技术 · 2026-06-10 08:33:43+08:00 · tech

之前有 3 个 Voice 号通过 web 版每月手动互发不同语种歌词。 三月底互发时,一个近十年的号和一个十多年的号在各发送一条信息后 voice 账号被禁用了。 多次去 Google 申诉都是同样封禁不给用。去 FCC 投诉,Google 拖到最后一天回复让通过链接地址去申诉,结果还是不给解封。 为了暂时保住最后一个注册时间最长的 Voice 号,在刷了 xiaomi.eu 的 Note 12 Tubro 上安装了 Voice 应用,24 小时开启 SS 配置绕过大陆地址。 Voice 应用没有开启自启动,省电策略是后台运行超过 10 分钟后关闭。现状是不论停止运行多久只要收到信息都会第一时间推送通知。 本机上另外一个能抽风式推送通知的是智能省电策略的淘宝。其他 play 版应用即使点击打开都不会用推送通知。

LinuxDo 最新话题 · 2026-06-10 00:32:06+08:00 · tech

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>歌词搜索 & 格式转换 - LRCLib</title> <style> :root { --bg: #0b0b12; --surface: #161622; --surface2: #1e1e30; --border: #2a2a40; --text: #e0e0e0; --text2: #9090a8; --accent: #7c5cfc; --accent2: #a78bfa; --green: #34d399; --radius: 14px; --radius-sm: 8px; --radius-xs: 6px; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', 'Noto Sans SC', sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; display: flex; justify-content: center; padding: 24px 16px; } .container { width: 100%; max-width: 960px; display: flex; flex-direction: column; gap: 20px; } .header { text-align: center; padding: 8px 0; } .header h1 { font-size: 1.9rem; font-weight: 700; letter-spacing: -0.5px; background: linear-gradient(135deg, #a78bfa 0%, #34d399 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .header .subtitle { color: var(--text2); font-size: 0.85rem; margin-top: 2px; } .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 22px; } .search-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } .search-row input { flex: 1; min-width: 200px; padding: 12px 16px; border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--surface2); color: var(--text); font-size: 0.95rem; outline: none; transition: border-color 0.2s; } .search-row input:focus { border-color: var(--accent); } .search-row input::placeholder { color: #555; } .btn { padding: 11px 20px; border-radius: var(--radius-sm); border: none; cursor: pointer; font-size: 0.9rem; font-weight: 600; transition: all 0.2s; white-space: nowrap; display: inline-flex; align-items: center; gap: 6px; letter-spacing: 0.2px; } .btn-primary { background: var(--accent); color: #fff; } .btn-primary:hover { background: #8f6fff; transform: translateY(-1px); box-shadow: 0 6px 24px rgba(124, 92, 252, 0.35); } .btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); } .btn-outline:hover { background: var(--surface2); border-color: #555; } .btn-sm { padding: 7px 14px; font-size: 0.8rem; border-radius: var(--radius-xs); } .btn-xs { padding: 5px 10px; font-size: 0.74rem; border-radius: 5px; } .btn-copy { background: #065f46; color: #d1fae5; border: 1px solid #059669; } .btn-copy:hover { background: #047857; box-shadow: 0 4px 16px rgba(5, 150, 105, 0.3); } .btn-download { background: #1e3a5f; color: #bfdbfe; border: 1px solid #3b82f6; } .btn-download:hover { background: #1e40af; box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3); } .results-panel { display: none; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px 18px; max-height: 340px; overflow-y: auto; } .results-panel.active { display: block; } .results-panel .section-label { font-size: 0.78rem; color: var(--text2); text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; } .result-item { display: flex; align-items: center; justify-content: space-between; padding: 11px 14px; border-radius: var(--radius-sm); cursor: pointer; transition: background 0.15s; gap: 12px; flex-wrap: wrap; } .result-item:hover { background: var(--surface2); } .result-item+.result-item { border-top: 1px solid rgba(255, 255, 255, 0.04); } .result-info { flex: 1; min-width: 0; } .result-info .track { font-weight: 600; font-size: 0.98rem; color: #f0f0f0; } .result-info .artist { font-size: 0.83rem; color: var(--text2); } .result-info .album { font-size: 0.76rem; color: #666; } .result-meta { font-size: 0.75rem; color: #555; white-space: nowrap; } .placeholder-text { text-align: center; color: var(--text2); padding: 28px; font-size: 0.9rem; } .loading-indicator { text-align: center; padding: 28px; color: var(--text2); display: flex; align-items: center; justify-content: center; gap: 10px; } .spinner { width: 18px; height: 18px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.7s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .lyrics-panel { display: none; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; } .lyrics-panel.active { display: block; } .lyrics-topbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; padding: 18px 22px; border-bottom: 1px solid var(--border); } .lyrics-topbar .song-info h2 { font-size: 1.25rem; font-weight: 700; } .lyrics-topbar .song-info .meta-line { font-size: 0.84rem; color: var(--text2); margin-top: 2px; } .lyrics-topbar .song-info .meta-line .artist-name { color: var(--accent2); font-weight: 500; } .format-tabs { display: flex; gap: 4px; padding: 12px 22px; border-bottom: 1px solid var(--border); flex-wrap: wrap; background: rgba(0, 0, 0, 0.15); } .format-tab { padding: 9px 18px; border-radius: 22px; border: 1px solid transparent; background: transparent; color: var(--text2); cursor: pointer; font-size: 0.84rem; font-weight: 500; transition: all 0.2s; letter-spacing: 0.3px; } .format-tab.active { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; } .format-tab:hover:not(.active) { border-color: #555; color: #d0d0d0; } .lyrics-body { padding: 20px 22px; max-height: 520px; overflow-y: auto; background: var(--surface2); font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Consolas', 'PingFang SC', monospace; font-size: 0.88rem; line-height: 1.75; white-space: pre-wrap; color: #c8c8d8; } .lyrics-body .lrc-tag { color: var(--accent2); font-weight: 600; } .lyrics-body .ass-header { color: #fbbf24; } .lyrics-body .ass-style { color: #60a5fa; } .lyrics-body .srt-index { color: #94a3b8; } .lyrics-body .srt-time { color: #34d399; } .lyrics-body .vtt-header-line { color: #fbbf24; } .lyrics-body .vtt-cue-time { color: #34d399; } .action-bar { display: flex; gap: 10px; padding: 14px 22px; border-top: 1px solid var(--border); flex-wrap: wrap; align-items: center; background: rgba(0, 0, 0, 0.1); } .action-bar .action-label { font-size: 0.76rem; color: var(--text2); text-transform: uppercase; letter-spacing: 1.2px; font-weight: 600; margin-right: 4px; } .action-bar .divider { width: 1px; height: 20px; background: var(--border); margin: 0 6px; } .toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 12px 26px; border-radius: 30px; font-weight: 600; font-size: 0.88rem; z-index: 999; opacity: 0; pointer-events: none; transition: opacity 0.3s; letter-spacing: 0.3px; } .toast.show { opacity: 1; } .toast.success { background: #065f46; color: #d1fae5; box-shadow: 0 8px 28px rgba(5, 150, 105, 0.35); } .toast.error { background: #7f1d1d; color: #fecaca; box-shadow: 0 8px 28px rgba(220, 38, 38, 0.35); } ::-webkit-scrollbar { width: 5px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { background: #555; } @media (max-width: 640px) { .search-row { flex-direction: column; align-items: stretch; } .search-row input { min-width: 100%; } .btn { justify-content: center; } .lyrics-topbar { flex-direction: column; align-items: flex-start; } .action-bar .divider { display: none; } .format-tabs { gap: 2px; } .format-tab { padding: 7px 12px; font-size: 0.78rem; } } </style> </head> <body> <div class="container"> <div class="header"> <h1>🎵 歌词搜索 & 格式转换</h1> <p class="subtitle">基于 LRCLib · LRC / SRT / ASS / VTT 四种字幕歌词格式</p> </div> <div class="card"> <div class="search-row"> <input type="text" id="searchInput" placeholder="搜索歌曲名或歌手名… 或输入「歌曲名 - 歌手名」精确查找" autocomplete="off"> <button class="btn btn-primary" id="searchBtn">🔍 搜索</button> <button class="btn btn-outline btn-sm" id="directBtn">🎯 精确获取</button> </div> </div> <div class="results-panel" id="resultsPanel"> <div class="section-label">📋 搜索结果 <span id="resultCount"></span></div> <div id="resultsList"></div> </div> <div class="lyrics-panel" id="lyricsPanel"> <div class="lyrics-topbar"> <div class="song-info"> <h2 id="songTitle">—</h2> <div class="meta-line"> <span class="artist-name" id="songArtist">—</span> <span style="margin:0 6px;color:#555;">·</span> <span id="songAlbum">—</span> <span style="margin:0 6px;color:#555;">·</span> <span id="songDuration">—</span> </div> </div> </div> <div class="format-tabs"> <button class="format-tab active" data-format="lrc">📝 LRC 歌词</button> <button class="format-tab" data-format="srt">🎬 SRT 字幕</button> <button class="format-tab" data-format="ass">🎨 ASS 字幕</button> <button class="format-tab" data-format="vtt">🌐 VTT 字幕</button> </div> <div class="lyrics-body" id="lyricsBody"></div> <div class="action-bar"> <span class="action-label">当前格式</span> <button class="btn btn-copy btn-sm" id="copyBtn">📋 复制</button> <div class="divider"></div> <span class="action-label" style="color:#bfdbfe;">文件导出</span> <button class="btn btn-download btn-sm" id="downloadLrcBtn">⬇ .lrc</button> <button class="btn btn-download btn-sm" id="downloadSrtBtn">⬇ .srt</button> <button class="btn btn-download btn-sm" id="downloadAssBtn">⬇ .ass</button> <button class="btn btn-download btn-sm" id="downloadVttBtn">⬇ .vtt</button> </div> </div> </div> <div class="toast" id="toast"></div> <script> (function() { // ── DOM refs ──────────────────────────── var searchInput = document.getElementById('searchInput'); var searchBtn = document.getElementById('searchBtn'); var directBtn = document.getElementById('directBtn'); var resultsPanel = document.getElementById('resultsPanel'); var resultsList = document.getElementById('resultsList'); var resultCount = document.getElementById('resultCount'); var lyricsPanel = document.getElementById('lyricsPanel'); var lyricsBody = document.getElementById('lyricsBody'); var songTitle = document.getElementById('songTitle'); var songArtist = document.getElementById('songArtist'); var songAlbum = document.getElementById('songAlbum'); var songDuration = document.getElementById('songDuration'); var copyBtn = document.getElementById('copyBtn'); var downloadLrcBtn = document.getElementById('downloadLrcBtn'); var downloadSrtBtn = document.getElementById('downloadSrtBtn'); var downloadAssBtn = document.getElementById('downloadAssBtn'); var downloadVttBtn = document.getElementById('downloadVttBtn'); var toastEl = document.getElementById('toast'); var formatTabs = document.querySelectorAll('.format-tab'); // ── State ──────────────────────────────── var currentSong = null; var currentFormat = 'lrc'; var searchResults = []; // ── Toast ──────────────────────────────── var toastTimer; function showToast(msg, isError) { clearTimeout(toastTimer); toastEl.textContent = msg; toastEl.className = 'toast show ' + (isError ? 'error' : 'success'); toastTimer = setTimeout(function() { toastEl.className = 'toast'; }, 2200); } // ── API ────────────────────────────────── function apiSearch(query) { return fetch('https://lrclib.net/api/search?q=' + encodeURIComponent(query)) .then(function(resp) { if (!resp.ok) throw new Error('搜索失败 (HTTP ' + resp.status + ')'); return resp.json(); }); } function apiGetLyrics(track, artist) { return fetch( 'https://lrclib.net/api/get?track_name=' + encodeURIComponent(track) + '&artist_name=' + encodeURIComponent(artist) ) .then(function(resp) { if (!resp.ok) throw new Error('获取失败 (HTTP ' + resp.status + ')'); return resp.text(); }) .then(function(text) { if (!text) throw new Error('未找到该歌曲'); return JSON.parse(text); }); } // ── LRC Parsing ────────────────────────── function parseLrcTimestamps(syncedLyrics) { var items = []; if (!syncedLyrics) return items; var regex = /\[(\d{2}):(\d{2})\.(\d{2,3})\]\s*(.*)/g; var m; while ((m = regex.exec(syncedLyrics)) !== null) { var min = parseInt(m[1], 10); var sec = parseInt(m[2], 10); var fracRaw = m[3]; var ms = fracRaw.length === 2 ? parseInt(fracRaw, 10) * 10 : parseInt(fracRaw, 10); var totalMs = min * 60000 + sec * 1000 + ms; items.push({ timeMs: totalMs, text: (m[4] || '').trim() }); } return items; } function pad2(n) { return String(n).padStart(2, '0'); } function pad3(n) { return String(n).padStart(3, '0'); } function msToLrc(ms) { var min = Math.floor(ms / 60000); var sec = Math.floor((ms % 60000) / 1000); var cs = Math.floor((ms % 1000) / 10); return '[' + pad2(min) + ':' + pad2(sec) + '.' + pad2(cs) + ']'; } function msToSrt(ms) { var h = Math.floor(ms / 3600000); var m = Math.floor((ms % 3600000) / 60000); var s = Math.floor((ms % 60000) / 1000); var milli = ms % 1000; return pad2(h) + ':' + pad2(m) + ':' + pad2(s) + ',' + pad3(milli); } function msToAss(ms) { var h = Math.floor(ms / 3600000); var m = Math.floor((ms % 3600000) / 60000); var s = Math.floor((ms % 60000) / 1000); var cs = Math.floor((ms % 1000) / 10); return h + ':' + pad2(m) + ':' + pad2(s) + '.' + pad2(cs); } function msToVtt(ms) { var h = Math.floor(ms / 3600000); var m = Math.floor((ms % 3600000) / 60000); var s = Math.floor((ms % 60000) / 1000); var milli = ms % 1000; return pad2(h) + ':' + pad2(m) + ':' + pad2(s) + '.' + pad3(milli); } // ── Format Generators ──────────────────── function generateLRC(song) { var items = parseLrcTimestamps(song.syncedLyrics); var lines = []; lines.push('[ti:' + (song.trackName || '') + ']'); lines.push('[ar:' + (song.artistName || '') + ']'); if (song.albumName) lines.push('[al:' + song.albumName + ']'); if (song.duration) { var m = Math.floor(song.duration / 60); var s = Math.floor(song.duration % 60); lines.push('[length:' + pad2(m) + ':' + pad2(s) + ']'); } lines.push('[by:LRC Generator]'); lines.push(''); if (items.length > 0) { for (var i = 0; i < items.length; i++) { lines.push(msToLrc(items[i].timeMs) + (items[i].text || '♪')); } } else if (song.plainLyrics) { var plainLines = song.plainLyrics.split('\n'); for (var j = 0; j < plainLines.length; j++) { lines.push(plainLines[j].trim()); } } return lines.join('\n'); } function generateSRT(song) { var items = parseLrcTimestamps(song.syncedLyrics); if (items.length === 0) { if (song.plainLyrics) { return '1\n00:00:00,000 --> 00:03:00,000\n' + song.plainLyrics.trim() + '\n'; } return ''; } var lines = []; for (var i = 0; i < items.length; i++) { var cur = items[i]; var next = items[i + 1]; var endMs = next ? next.timeMs : cur.timeMs + 3000; lines.push(String(i + 1)); lines.push(msToSrt(cur.timeMs) + ' --> ' + msToSrt(endMs)); lines.push(cur.text || '♪'); lines.push(''); } return lines.join('\n').trim(); } function generateASS(song) { var items = parseLrcTimestamps(song.syncedLyrics); var title = song.trackName || 'Unknown'; var artist = song.artistName || 'Unknown'; var lines = []; lines.push('[Script Info]'); lines.push('Title: ' + title); lines.push('Original Script: ' + artist); lines.push('ScriptType: v4.00+'); lines.push('Collisions: Normal'); lines.push('PlayDepth: 0'); lines.push(''); lines.push('[V4+ Styles]'); lines.push( 'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding'); lines.push( 'Style: Default,Microsoft YaHei,36,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,1,2,1,2,30,30,30,1'); lines.push(''); lines.push('[Events]'); lines.push('Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text'); if (items.length > 0) { for (var i = 0; i < items.length; i++) { var cur = items[i]; var next = items[i + 1]; var endMs = next ? next.timeMs : cur.timeMs + 3000; var text = (cur.text || '♪').replace(/\n/g, '\\N'); lines.push('Dialogue: 0,' + msToAss(cur.timeMs) + ',' + msToAss(endMs) + ',Default,,0,0,0,,' + text); } } else if (song.plainLyrics) { var plainLines = song.plainLyrics.split('\n').filter(function(l) { return l.trim(); }); var duration = (song.duration || 180) * 1000; var eachMs = Math.floor(duration / Math.max(plainLines.length, 1)); for (var j = 0; j < plainLines.length; j++) { var start = j * eachMs; var end = start + eachMs; var t = plainLines[j].trim().replace(/\n/g, '\\N'); lines.push('Dialogue: 0,' + msToAss(start) + ',' + msToAss(end) + ',Default,,0,0,0,,' + t); } } return lines.join('\n'); } function generateVTT(song) { var items = parseLrcTimestamps(song.syncedLyrics); var lines = []; lines.push('WEBVTT'); lines.push(''); if (items.length > 0) { for (var i = 0; i < items.length; i++) { var cur = items[i]; var next = items[i + 1]; var endMs = next ? next.timeMs : cur.timeMs + 3000; lines.push(msToVtt(cur.timeMs) + ' --> ' + msToVtt(endMs)); lines.push(cur.text || '♪'); lines.push(''); } } else if (song.plainLyrics) { var plainLines = song.plainLyrics.split('\n').filter(function(l) { return l.trim(); }); var duration = (song.duration || 180) * 1000; var eachMs = Math.floor(duration / Math.max(plainLines.length, 1)); for (var j = 0; j < plainLines.length; j++) { var start = j * eachMs; var end = start + eachMs; lines.push(msToVtt(start) + ' --> ' + msToVtt(end)); lines.push(plainLines[j].trim()); lines.push(''); } } return lines.join('\n').trim(); } function getFormattedContent(format, song) { if (!song) return ''; switch (format) { case 'lrc': return generateLRC(song); case 'srt': return generateSRT(song); case 'ass': return generateASS(song); case 'vtt': return generateVTT(song); default: return ''; } } function escapeHtml(str) { var div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function getHighlightedHtml(format, song) { var raw = getFormattedContent(format, song); if (!raw) return '<span style="color:#666;">暂无内容</span>'; var esc = escapeHtml(raw); switch (format) { case 'lrc': esc = esc.replace( /^(\[ti:.*\]|\[ar:.*\]|\[al:.*\]|\[length:.*\]|\[by:.*\])$/gm, '<span class="lrc-tag">$1</span>' ); esc = esc.replace( /^(\[\d{2}:\d{2}\.\d{2}\])/gm, '<span class="lrc-tag">$1</span>' ); break; case 'srt': esc = esc.replace(/^(\d+)$/gm, '<span class="srt-index">$1</span>'); esc = esc.replace( /^(\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3})$/gm, '<span class="srt-time">$1</span>' ); break; case 'ass': esc = esc.replace(/^(\[.*\])$/gm, '<span class="ass-header">$1</span>'); esc = esc.replace(/^(Style:.*)$/gm, '<span class="ass-style">$1</span>'); esc = esc.replace(/^(Format:.*)$/gm, '<span class="ass-style">$1</span>'); break; case 'vtt': esc = esc.replace(/^(WEBVTT)$/gm, '<span class="vtt-header-line">$1</span>'); esc = esc.replace( /^(\d{2}:\d{2}:\d{2}\.\d{3} --> \d{2}:\d{2}:\d{2}\.\d{3})$/gm, '<span class="vtt-cue-time">$1</span>' ); break; } return esc; } // ── Render ──────────────────────────────── function renderLyrics(song) { currentSong = song; songTitle.textContent = song.trackName || '未知歌曲'; songArtist.textContent = song.artistName || '未知歌手'; songAlbum.textContent = song.albumName || ''; songDuration.textContent = formatDuration(song.duration); lyricsPanel.classList.add('active'); updateLyricsDisplay(); lyricsPanel.scrollIntoView({ behavior: 'smooth', block: 'center' }); } function updateLyricsDisplay() { lyricsBody.innerHTML = getHighlightedHtml(currentFormat, currentSong); } function formatDuration(sec) { if (!sec && sec !== 0) return ''; var m = Math.floor(sec / 60); var s = Math.floor(sec % 60); return m + ':' + pad2(s); } // ── Tab Switching ───────────────────────── for (var t = 0; t < formatTabs.length; t++) { formatTabs[t].addEventListener('click', function() { for (var i = 0; i < formatTabs.length; i++) { formatTabs[i].classList.remove('active'); } this.classList.add('active'); currentFormat = this.dataset.format; updateLyricsDisplay(); }); } // ── Copy ────────────────────────────────── copyBtn.addEventListener('click', function() { if (!currentSong) { showToast('请先搜索并选择一首歌曲', true); return; } var content = getFormattedContent(currentFormat, currentSong); var labels = { lrc: 'LRC', srt: 'SRT', ass: 'ASS', vtt: 'VTT' }; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(content).then(function() { showToast('✅ 已复制 ' + labels[currentFormat] + ' 内容'); }).catch(function() { fallbackCopy(content); showToast('✅ 已复制 ' + labels[currentFormat] + ' 内容'); }); } else { fallbackCopy(content); showToast('✅ 已复制 ' + labels[currentFormat] + ' 内容'); } }); function fallbackCopy(text) { var ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;opacity:0;'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } // ── Download ────────────────────────────── function downloadFile(content, filename, mime) { var blob = new Blob([content], { type: mime }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function safeFilename(song, ext) { var t = (song.trackName || 'unknown').replace(/[\\/:*?"<>|]/g, '_'); var a = (song.artistName || 'unknown').replace(/[\\/:*?"<>|]/g, '_'); return a + ' - ' + t + '.' + ext; } function doDownload(format) { if (!currentSong) { showToast('请先选择歌曲', true); return; } var content = getFormattedContent(format, currentSong); if (!content.trim()) { showToast('没有可导出的内容', true); return; } var mimes = { lrc: 'text/plain', srt: 'text/srt', ass: 'text/plain', vtt: 'text/vtt' }; downloadFile(content, safeFilename(currentSong, format), mimes[format] || 'text/plain'); showToast('⬇ 已下载 ' + format.toUpperCase() + ' 文件'); } downloadLrcBtn.addEventListener('click', function() { doDownload('lrc'); }); downloadSrtBtn.addEventListener('click', function() { doDownload('srt'); }); downloadAssBtn.addEventListener('click', function() { doDownload('ass'); }); downloadVttBtn.addEventListener('click', function() { doDownload('vtt'); }); // ── Search ──────────────────────────────── function doSearch(query) { if (!query || !query.trim()) { showToast('请输入搜索关键词', true); return; } resultsList.innerHTML = '<div class="loading-indicator"><span class="spinner"></span>搜索中…</div>'; resultsPanel.classList.add('active'); apiSearch(query.trim()) .then(function(results) { searchResults = results; resultCount.textContent = '(' + results.length + ' 条)'; if (results.length === 0) { resultsList.innerHTML = '<div class="placeholder-text">😕 未找到匹配的歌曲,换个关键词试试</div>'; return; } var html = ''; for (var i = 0; i < results.length; i++) { var r = results[i]; html += '<div class="result-item" data-index="' + i + '">'; html += '<div class="result-info">'; html += '<div class="track">' + escapeHtml(r.trackName || r.name || '未知') + '</div>'; html += '<div class="artist">' + escapeHtml(r.artistName || '未知歌手') + '</div>'; if (r.albumName) { html += '<div class="album">💿 ' + escapeHtml(r.albumName) + '</div>'; } html += '</div>'; html += '<div class="result-meta">' + formatDuration(r.duration) + '</div>'; html += '<button class="btn btn-outline btn-xs pick-btn" data-index="' + i + '">选择</button>'; html += '</div>'; } resultsList.innerHTML = html; var items = resultsList.querySelectorAll('.result-item'); for (var j = 0; j < items.length; j++) { (function(idx) { items[j].addEventListener('click', function(e) { if (e.target.closest('.pick-btn')) return; loadResult(idx); }); })(j); } var btns = resultsList.querySelectorAll('.pick-btn'); for (var k = 0; k < btns.length; k++) { (function(idx) { btns[k].addEventListener('click', function(e) { e.stopPropagation(); loadResult(idx); }); })(k); } }) .catch(function(err) { resultsList.innerHTML = '<div class="placeholder-text">❌ ' + escapeHtml(err.message) + '</div>'; resultCount.textContent = '(0 条)'; }); } function loadResult(idx) { var song = searchResults[idx]; if (!song) return; var tn = song.trackName || song.name; var an = song.artistName || ''; var allItems = resultsList.querySelectorAll('.result-item'); for (var i = 0; i < allItems.length; i++) { allItems[i].style.opacity = '0.4'; } var target = resultsList.querySelector('[data-index="' + idx + '"]'); if (target) target.style.opacity = '1'; apiGetLyrics(tn, an) .then(function(data) { renderLyrics(data); }) .catch(function(err) { showToast('❌ ' + err.message, true); }) .finally(function() { for (var j = 0; j < allItems.length; j++) { allItems[j].style.opacity = '1'; } }); } // ── Event Bindings ──────────────────────── searchBtn.onclick = function() { doSearch(searchInput.value); }; searchInput.onkeydown = function(e) { if (e.key === 'Enter') { doSearch(searchInput.value); } }; directBtn.onclick = function() { var q = searchInput.value.trim(); if (!q) { showToast('请输入「歌曲名 - 歌手名」', true); return; } var seps = [' - ', '-', ' – ', '–', ' | ', '|', ':', ':']; var track = ''; var artist = ''; for (var i = 0; i < seps.length; i++) { if (q.indexOf(seps[i]) !== -1) { var parts = q.split(seps[i]); track = parts[0].trim(); artist = parts.slice(1).join(seps[i]).trim(); break; } } if (!track) { track = prompt('请输入歌曲名:', q); if (!track) return; artist = prompt('请输入歌手名(可选):', '') || ''; } resultsList.innerHTML = '<div class="loading-indicator"><span class="spinner"></span>获取中…</div>'; resultsPanel.classList.add('active'); apiGetLyrics(track, artist) .then(function(data) { searchResults = [data]; resultCount.textContent = '(1 条)'; resultsList.innerHTML = '<div class="result-item" style="opacity:1;">' + '<div class="result-info">' + '<div class="track">' + escapeHtml(data.trackName || data.name || '未知') + '</div>' + '<div class="artist">' + escapeHtml(data.artistName || '未知歌手') + '</div>' + (data.albumName ? '<div class="album">💿 ' + escapeHtml(data.albumName) + '</div>' : '') + '</div>' + '<div class="result-meta">' + formatDuration(data.duration) + '</div>' + '</div>'; renderLyrics(data); }) .catch(function(err) { resultsList.innerHTML = '<div class="placeholder-text">❌ ' + escapeHtml(err.message) + '</div>'; resultCount.textContent = '(0 条)'; showToast('❌ ' + err.message, true); }); }; // ── Init ────────────────────────────────── searchInput.focus(); })(); </script> </body> </html> 1 个帖子 - 1 位参与者 阅读完整话题