<!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 位参与者