WWW.YOUINFO.SITE
标签聚合 flac

/tag/flac

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 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-10 00:28:35+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 重命名工具 · 读取内嵌元数据</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #f5f0e8; font-family: 'Segoe UI', system-ui, sans-serif; min-height: 100vh; display: flex; justify-content: center; align-items: center; padding: 1.5rem; } .container { max-width: 850px; width: 100%; background: #fffdf7; border-radius: 1.8rem; box-shadow: 0 20px 40px rgba(0,0,0,0.1); padding: 2rem; } h1 { font-size: 2rem; color: #4a3724; display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.3rem; } .badge { background: #d4b896; color: #2d1f0e; font-size: 0.85rem; padding: 0.3rem 1rem; border-radius: 20px; font-weight: 600; } .desc { color: #6b5d4b; margin-bottom: 1.5rem; font-size: 0.95rem; border-left: 3px solid #c9a87c; padding-left: 1rem; } .drop-area { background: #faf7f1; border: 2px dashed #c8b28b; border-radius: 1.5rem; padding: 2.5rem; text-align: center; cursor: pointer; transition: all 0.2s; margin-bottom: 1.5rem; } .drop-area:hover, .drop-area.active { border-color: #a0845c; background: #f5ede0; box-shadow: 0 0 0 4px rgba(180,140,90,0.1); } .drop-area .icon { font-size: 3rem; margin-bottom: 0.5rem; } .drop-area .main-text { font-weight: 600; color: #4d3a24; font-size: 1.1rem; } .drop-area .sub-text { color: #8b7a62; font-size: 0.9rem; margin-top: 0.2rem; } input[type="file"] { display: none; } .file-panel { background: #fefcf8; border: 1px solid #e3d5bd; border-radius: 1.2rem; padding: 1rem; max-height: 380px; overflow-y: auto; margin: 1.2rem 0; display: none; } .file-row { display: flex; align-items: center; gap: 0.8rem; padding: 0.7rem 0.5rem; border-bottom: 1px solid #efe4d0; flex-wrap: wrap; } .file-row:last-child { border-bottom: none; } .orig { font-family: 'Consolas', 'Monaco', monospace; background: #f3ecdd; padding: 0.3rem 0.8rem; border-radius: 20px; font-size: 0.85rem; color: #5c4a30; word-break: break-all; flex: 1; min-width: 130px; } .arrow { color: #b39260; font-weight: bold; font-size: 1.2rem; } .newname { font-family: 'Consolas', 'Monaco', monospace; background: #e2edda; padding: 0.3rem 0.8rem; border-radius: 20px; font-size: 0.85rem; color: #2d4a1e; font-weight: 600; word-break: break-all; flex: 1; min-width: 130px; text-align: right; } .newname.missing { background: #ffe8e0; color: #a04030; font-style: italic; } .meta-detail { font-size: 0.7rem; color: #8b7356; background: #f9f4ea; padding: 0.15rem 0.6rem; border-radius: 12px; white-space: nowrap; } .actions { display: flex; gap: 0.8rem; flex-wrap: wrap; align-items: center; } button { padding: 0.8rem 1.8rem; border-radius: 2rem; border: 1px solid #d4bc92; background: #f1e7d4; color: #4d3a22; font-weight: 600; cursor: pointer; font-size: 0.95rem; transition: 0.2s; display: flex; align-items: center; gap: 0.3rem; } button:hover:not(:disabled) { background: #e5d3b0; } button.primary { background: #c7a16b; border-color: #9c7a4a; color: #fffdf5; box-shadow: 0 4px 12px rgba(160,120,50,0.2); } button.primary:hover:not(:disabled) { background: #b38845; } button:disabled { opacity: 0.45; cursor: not-allowed; } .status { margin-left: auto; font-size: 0.9rem; color: #6b5d48; background: #f6f1e6; padding: 0.4rem 1.2rem; border-radius: 20px; } .footer { margin-top: 1rem; font-size: 0.8rem; color: #9b8b74; text-align: center; } </style> </head> <body> <div class="container"> <h1>🎵 FLAC 元数据重命名 <span class="badge">标题-作曲家</span></h1> <div class="desc">读取 FLAC 文件内嵌的歌曲标题 (TITLE) 和作曲家 (COMPOSER/ARTIST) 标签,自动重命名为 "标题-作曲家.flac"</div> <div class="drop-area" id="dropZone"> <div class="icon">📂</div> <div class="main-text">点击选择或拖拽 FLAC 文件</div> <div class="sub-text">支持批量 · 读取内嵌元数据标签</div> </div> <input type="file" id="fileInput" accept=".flac,audio/flac" multiple> <div class="file-panel" id="filePanel"> <div id="fileList"></div> </div> <div class="actions"> <button id="clearBtn" disabled>🗑️ 清空</button> <button id="renameBtn" class="primary" disabled>💾 下载重命名文件</button> <span class="status" id="status">等待添加 FLAC 文件...</span> </div> <div class="footer">* 浏览器安全限制:通过下载方式生成新文件名,原文件不会被修改</div> </div> <script> (function() { const dropZone = document.getElementById('dropZone'); const fileInput = document.getElementById('fileInput'); const filePanel = document.getElementById('filePanel'); const fileListDiv = document.getElementById('fileList'); const clearBtn = document.getElementById('clearBtn'); const renameBtn = document.getElementById('renameBtn'); const statusEl = document.getElementById('status'); let filesData = []; // { file, title, composer, newName, error } // ========== 解析 FLAC 内嵌元数据 (Vorbis Comment) ========== async function readFlacMetadata(file) { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = (e) => { try { const buf = e.target.result; const view = new DataView(buf); if (buf.byteLength < 4) return resolve({ title: null, composer: null, error: '文件过小' }); const magic = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3)); if (magic !== 'fLaC') return resolve({ title: null, composer: null, error: '非FLAC文件' }); let offset = 4; let lastBlock = false; let title = null; let composer = null; while (offset < buf.byteLength && !lastBlock) { if (offset + 4 > buf.byteLength) break; const header = view.getUint8(offset); lastBlock = (header & 0x80) !== 0; const blockType = header & 0x7F; const blockSize = (view.getUint8(offset+1) << 16) | (view.getUint8(offset+2) << 8) | view.getUint8(offset+3); offset += 4; if (blockType === 4 && offset + blockSize <= buf.byteLength) { // VORBIS_COMMENT 块 const block = new Uint8Array(buf, offset, blockSize); const dec = new TextDecoder('utf-8'); if (block.length < 4) { offset += blockSize; continue; } const vendorLen = block[0] | (block[1]<<8) | (block[2]<<16) | (block[3]<<24); let pos = 4 + vendorLen; if (pos + 4 > block.length) { offset += blockSize; continue; } const numComments = block[pos] | (block[pos+1]<<8) | (block[pos+2]<<16) | (block[pos+3]<<24); pos += 4; for (let i = 0; i < numComments; i++) { if (pos + 4 > block.length) break; const commentLen = block[pos] | (block[pos+1]<<8) | (block[pos+2]<<16) | (block[pos+3]<<24); pos += 4; if (pos + commentLen > block.length) break; const commentStr = dec.decode(block.slice(pos, pos + commentLen)); pos += commentLen; const eqIdx = commentStr.indexOf('='); if (eqIdx > 0) { const key = commentStr.substring(0, eqIdx).toUpperCase().trim(); const val = commentStr.substring(eqIdx + 1).trim(); if (key === 'TITLE' && !title) title = val; // 优先 COMPOSER,其次 ARTIST if (key === 'COMPOSER' && !composer) composer = val; if (key === 'ARTIST' && !composer) composer = val; } } // 找到注释块即可退出 break; } offset += blockSize; } resolve({ title: title || null, composer: composer || null, error: null }); } catch (err) { resolve({ title: null, composer: null, error: err.message }); } }; reader.onerror = () => resolve({ title: null, composer: null, error: '读取失败' }); reader.readAsArrayBuffer(file); }); } // ========== 生成新文件名 ========== function makeNewName(title, composer, origName) { const sanitize = (s) => s.replace(/[\\/:*?"<>|]/g, '_').replace(/\s+/g, ' ').trim().substring(0, 200); const fallback = origName.replace(/\.flac$/i, '') || 'unknown'; const t = (title && title.trim()) ? sanitize(title) : sanitize(fallback); const c = (composer && composer.trim()) ? sanitize(composer) : '未知作曲家'; return `${t}-${c}.flac`; } // ========== 渲染文件列表 ========== function render() { fileListDiv.innerHTML = ''; if (filesData.length === 0) { filePanel.style.display = 'none'; clearBtn.disabled = true; renameBtn.disabled = true; setStatus('📭 暂无文件'); return; } filePanel.style.display = 'block'; clearBtn.disabled = false; renameBtn.disabled = false; filesData.forEach(item => { const row = document.createElement('div'); row.className = 'file-row'; const origSpan = document.createElement('span'); origSpan.className = 'orig'; origSpan.textContent = item.file.name; const arrow = document.createElement('span'); arrow.className = 'arrow'; arrow.textContent = '→'; const newSpan = document.createElement('span'); newSpan.className = 'newname'; if (item.error && !item.title && !item.composer) { newSpan.classList.add('missing'); newSpan.textContent = '⚠ 元数据缺失'; } else { newSpan.textContent = item.newName; } // 显示读取到的标签详情 const metaDetail = document.createElement('span'); metaDetail.className = 'meta-detail'; const t = item.title || '—'; const c = item.composer || '—'; metaDetail.textContent = `TITLE: ${t} | COMPOSER: ${c}`; row.appendChild(origSpan); row.appendChild(arrow); row.appendChild(newSpan); row.appendChild(metaDetail); fileListDiv.appendChild(row); }); const valid = filesData.filter(f => f.title || f.composer).length; setStatus(`📋 ${filesData.length} 个文件 · ${valid} 个可重命名`); } function setStatus(msg) { statusEl.textContent = msg; } // ========== 处理添加文件 ========== async function addFiles(fileList) { const flacFiles = Array.from(fileList).filter(f => f.name.toLowerCase().endsWith('.flac')); if (flacFiles.length === 0) { setStatus('❌ 请选择 .flac 文件'); return; } setStatus('🔍 正在读取内嵌元数据...'); renameBtn.disabled = true; clearBtn.disabled = true; for (const file of flacFiles) { const meta = await readFlacMetadata(file); const newName = makeNewName(meta.title, meta.composer, file.name); filesData.push({ file, title: meta.title, composer: meta.composer, newName, error: meta.error }); } // 去重 const seen = new Map(); filesData = filesData.filter(item => { const key = `${item.file.name}|${item.file.size}|${item.file.lastModified}`; if (seen.has(key)) return false; seen.set(key, true); return true; }); render(); setStatus('✅ 元数据读取完成'); } // ========== 执行下载重命名 ========== async function doRename() { const valid = filesData.filter(f => f.title || f.composer); if (valid.length === 0) { setStatus('⚠️ 没有可重命名的文件'); return; } setStatus('⏳ 正在下载重命名文件...'); renameBtn.disabled = true; clearBtn.disabled = true; for (const item of valid) { const blob = new Blob([item.file], { type: 'audio/flac' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = item.newName; document.body.appendChild(a); a.click(); document.body.removeChild(a); await new Promise(r => setTimeout(r, 150)); URL.revokeObjectURL(url); } setStatus('🎉 下载完成!原文件未被修改'); renameBtn.disabled = false; clearBtn.disabled = false; } function clearAll() { filesData = []; fileInput.value = ''; render(); setStatus('🧹 已清空'); } // ========== 事件绑定 ========== dropZone.addEventListener('click', () => fileInput.click()); dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('active'); }); dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('active'); }); dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('active'); if (e.dataTransfer.files.length) addFiles(e.dataTransfer.files); }); fileInput.addEventListener('change', e => { if (e.target.files.length) addFiles(e.target.files); }); clearBtn.addEventListener('click', clearAll); renameBtn.addEventListener('click', doRename); render(); setStatus('📎 选择或拖入 FLAC 文件以读取内嵌元数据'); })(); </script> </body> </html> 1 个帖子 - 1 位参与者 阅读完整话题