分享一个让你再也不愁歌词问题的脚本

分享一个让你再也不愁歌词问题的脚本
分享一个让你再也不愁歌词问题的脚本
<!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 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文