分享一个html网页音乐播放器

这次升级得相对比较完美了,可以本地添加歌曲还有网易云api拉取歌曲及歌词功能,自己用完全够了,就是要自己搭建网易云音乐api, 将代码中的****号替换为自己搭建的api地址就行了。 api搭建项目:https://gitlab.com/Binaryify/NeteaseCloudMusicApi 我是用vercel搭建的api(原谅无法公开分享),有流量限制100G每个月,自己用是完全够了,给个代码给喜欢折腾的佬友。目前不爽的是网易云好多歌曲没有版权无法播放,拉取下来不能播放的会自动跳过,只播放有版本的,希望哪位佬能指点一下,有个UnblockNeteaseMusic项目来解决这个问题,但目前我是没折腾出来方法。有佬友能解决的可以发个教程。
使用方法:将代码保存在 记事本 文件里面,把后缀改为html就可以了。兄弟们点个赞啊

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>音乐播放器 (桌面/移动端优化)</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
    <style>
        :root {
            --font-main: 'Noto Sans SC', 'Segoe UI', Arial, sans-serif;
            --bg-gradient: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
            --container-bg: rgba(255, 255, 255, 0.6);
            --container-shadow: rgba(0, 0, 0, 0.1);
            --backdrop-blur: 12px;
            --text-color: #2c3e50;
            --text-secondary-color: #7f8c8d;
            --primary-color: #3498db;
            --primary-color-dark: #2980b9;
            --warning-color: #e74c3c;
            --item-hover-bg: rgba(52, 152, 219, 0.1);
            --item-current-bg: rgba(52, 152, 219, 0.2);
            --item-current-text: var(--primary-color);
            --border-color: rgba(0, 0, 0, 0.1);
            --lyrics-highlight-bg: rgba(46, 204, 113, 0.15);
            --lyrics-highlight-text: #27ae60;
            --lyrics-highlight-shadow: rgba(46, 204, 113, 0.2);
            --component-bg: rgba(255, 255, 255, 0.5);
            --scrollbar-thumb-bg: rgba(0, 0, 0, 0.2);
            --scrollbar-track-bg: transparent;
        }

        .dark-mode {
            --bg-gradient: linear-gradient(135deg, #0f2027 0%, #203a43 50%, #2c5364 100%);
            --container-bg: rgba(30, 30, 30, 0.6);
            --container-shadow: rgba(0, 0, 0, 0.3);
            --text-color: #ecf0f1;
            --text-secondary-color: #95a5a6;
            --primary-color: #5dade2;
            --primary-color-dark: #3498db;
            --item-hover-bg: rgba(93, 173, 226, 0.1);
            --item-current-bg: rgba(93, 173, 226, 0.2);
            --border-color: rgba(255, 255, 255, 0.15);
            --lyrics-highlight-bg: rgba(26, 188, 156, 0.15);
            --lyrics-highlight-text: #1abc9c;
            --component-bg: rgba(44, 44, 44, 0.5);
            --scrollbar-thumb-bg: rgba(255, 255, 255, 0.2);
        }

        ::-webkit-scrollbar { width: 6px; }
        ::-webkit-scrollbar-track { background: var(--scrollbar-track-bg); }
        ::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb-bg); border-radius: 3px; }
        ::-webkit-scrollbar-thumb:hover { background: var(--primary-color); }

        body {
            font-family: var(--font-main);
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            padding: 20px;
            box-sizing: border-box;
            background-image: var(--bg-gradient);
            background-attachment: fixed;
            color: var(--text-color);
            transition: background 0.5s, color 0.5s;
        }

        .container {
            background: var(--container-bg);
            backdrop-filter: blur(var(--backdrop-blur));
            -webkit-backdrop-filter: blur(var(--backdrop-blur));
            padding: 35px;
            border-radius: 24px;
            box-shadow: 0 15px 30px var(--container-shadow);
            border: 1px solid var(--border-color);
            width: 100%;
            max-width: 850px;
            display: grid;
            /* 桌面端布局 */
            grid-template-rows: auto auto 1fr auto;
            gap: 20px;
            transition: background 0.5s, box-shadow 0.5s;
            height: 90vh;
            max-height: 700px;
        }

        .header { text-align: center; position: relative; }
        .header h1 { margin: 0; font-size: 2.2em; font-weight: 700; color: var(--primary-color); letter-spacing: 1px; }
        .header .warning { color: var(--text-secondary-color); font-size: 0.9em; margin-top: 10px; font-style: italic; }
        
        .file-input-area {
            background: var(--component-bg);
            border: 2px dashed var(--border-color);
            border-radius: 16px;
            padding: 20px;
            text-align: center;
            cursor: pointer;
            transition: all 0.3s ease;
        }
        .file-input-area:hover, .file-input-area.dragover { border-color: var(--primary-color); background: var(--item-hover-bg); }
        .file-input-area .file-input-label { display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--text-secondary-color); }
        .file-input-label i { font-size: 2em; margin-bottom: 10px; color: var(--primary-color); }
        
        .view-toggle { display: none; }

        .main-content { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; overflow: hidden; }
        .playlist, .lyrics { background: var(--component-bg); border-radius: 16px; padding: 20px; border: 1px solid var(--border-color); height: 100%; overflow-y: auto; }
        
        .playlist div { padding: 12px 15px; border-radius: 10px; transition: all 0.2s ease-in-out; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        .playlist div:hover { background-color: var(--item-hover-bg); color: var(--item-current-text); transform: translateX(5px); }
        .playlist .current { color: var(--item-current-text); font-weight: 500; background-color: var(--item-current-bg); }
        
        .lyrics { text-align: center; font-size: 1.1em; line-height: 2.2; }
        .lyrics div { white-space: pre-wrap; }
        .lyrics .current { color: var(--lyrics-highlight-text); font-weight: 700; background-color: var(--lyrics-highlight-bg); transform: scale(1.05); box-shadow: 0 4px 15px var(--lyrics-highlight-shadow); }

        .controls { display: flex; justify-content: center; align-items: center; gap: 20px; flex-wrap: wrap; }
        .controls button { background: var(--primary-color); color: white; border: none; border-radius: 50%; cursor: pointer; font-size: 1.2em; width: 50px; height: 50px; display: flex; justify-content: center; align-items: center; transition: all 0.2s ease; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
        .controls button:hover { background: var(--primary-color-dark); transform: translateY(-2px); box-shadow: 0 6px 15px rgba(0,0,0,0.15); }
        .controls button:active { transform: translateY(0); box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
        .controls button#loadOnlineBtn { width: auto; border-radius: 25px; padding: 0 25px; gap: 10px; }
        .controls button:disabled { background: var(--text-secondary-color); cursor: not-allowed; transform: none; box-shadow: none; }
        .controls audio { flex-grow: 1; min-width: 300px; }
        
        .loader { width: 20px; height: 20px; border: 3px solid var(--primary-color); border-bottom-color: transparent; border-radius: 50%; display: inline-block; box-sizing: border-box; animation: rotation 1s linear infinite; }
        @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
        
        /* [优化] 更小、更精致的主题切换按钮 */
        .theme-switch-wrapper { position: absolute; top: 0px; right: 0px; }
        .theme-switch { display: inline-block; height: 24px; position: relative; width: 48px; }
        .theme-switch input { display:none; }
        .slider { background-color: #ccc; bottom: 0; cursor: pointer; left: 0; position: absolute; right: 0; top: 0; transition: .4s; border-radius: 24px; }
        .slider:before { background-color: #fff; bottom: 3px; content: ""; height: 18px; left: 3px; position: absolute; transition: .4s; width: 18px; border-radius: 50%; }
        .slider .fa-sun, .slider .fa-moon { position: absolute; top: 50%; transform: translateY(-50%); color: #fff; font-size: 12px; opacity: 0; transition: opacity 0.4s; }
        .slider .fa-sun { left: 6px; opacity: 1; }
        .slider .fa-moon { right: 6px; }
        input:checked + .slider { background-color: var(--primary-color); }
        input:checked + .slider:before { transform: translateX(24px); }
        input:checked + .slider .fa-sun { opacity: 0; }
        input:checked + .slider .fa-moon { opacity: 1; }

        /* --- [手机版优化] 媒体查询 --- */
        @media (max-width: 768px) {
            body { padding: 0; }
            .container {
                padding: 15px;
                gap: 15px;
                grid-template-rows: auto auto auto 1fr auto;
                border-radius: 0;
                border: none;
                height: 100vh;
                max-height: none;
            }
            .header h1 { font-size: 1.8em; }
            .file-input-area { padding: 15px; }
            .file-input-label i { font-size: 1.8em; }
            
            .view-toggle {
                display: flex;
                gap: 10px;
            }
            .view-toggle button {
                flex-grow: 1;
                background: var(--component-bg);
                color: var(--text-secondary-color);
                border: 1px solid var(--border-color);
                padding: 10px;
                border-radius: 10px;
                font-size: 0.9em;
                font-family: inherit;
                cursor: pointer;
                transition: all 0.3s ease;
            }
            .view-toggle button.active {
                background: var(--primary-color);
                color: white;
                font-weight: 500;
                border-color: var(--primary-color);
            }
            
            /* [修复-1] 修复列表撑开布局问题:确保主内容区高度受控,内部元素滚动 */
            .main-content {
                display: flex; /* 改为flex以约束子元素 */
                grid-template-columns: none; /* 取消桌面端的分栏 */
            }
            .playlist, .lyrics {
                width: 100%;
                flex-shrink: 0; /* 防止被压缩 */
            }
            /* [修复-2] 移动端默认隐藏歌词 */
            .mobile-hidden {
                display: none !important;
            }

            /* [优化-3] 优化手机歌词行高,显示更多内容 */
            .lyrics {
                line-height: 1.8;
                font-size: 1em;
            }

            /* [优化-5] 重新设计移动端控制器布局 */
            .controls {
                flex-wrap: wrap;
                gap: 10px;
                align-items: center;
            }
            .controls audio {
                order: -1; /* 进度条置顶 */
                width: 100%;
                min-width: unset;
            }
            .controls button {
                font-size: 1.1em;
                width: 48px;
                height: 48px;
            }
            .controls button#loadOnlineBtn {
                flex-grow: 1;
                width: auto;
                max-width: 180px;
            }
        }
    </style>
</head>
<body>
<div class="container">

    <div class="header">
        <div class="theme-switch-wrapper">
            <label class="theme-switch" for="checkbox"><input type="checkbox" id="checkbox" /><div class="slider"><i class="fas fa-sun"></i><i class="fas fa-moon"></i></div></label>
        </div>
        <h1>音乐播放器</h1>
        <div class="warning">支持本地音乐与在线雷达探索</div>
    </div>
    
    <div class="file-input-area" id="fileDropArea">
        <input type="file" id="fileInput" multiple accept="audio/*,.lrc,.txt" style="display: none;">
        <label for="fileInput" class="file-input-label"><i class="fas fa-folder-plus"></i><span>点击选择 或 拖放本地音乐文件至此</span></label>
    </div>
    
    <div class="view-toggle">
        <button id="showPlaylistBtn" class="active">播放列表</button>
        <button id="showLyricsBtn">歌词</button>
    </div>

    <div class="main-content">
        <div class="playlist" id="playlist">请选择本地音乐或探索在线雷达</div>
        <div class="lyrics mobile-hidden" id="lyrics">歌词将在此处同步显示</div>
    </div>

    <div class="controls">
        <button onclick="playPrevious()" title="上一曲"><i class="fas fa-backward-step"></i></button>
        <audio id="audioPlayer" controls></audio>
        <button onclick="playNext()" title="下一曲"><i class="fas fa-forward-step"></i></button>
        <button id="loadOnlineBtn" title="聚合所有雷达,探索新音乐">
            <span class="btn-text"><i class="fas fa-satellite-dish"></i> 探索雷达</span>
            <span class="loader" style="display: none;"></span>
        </button>
    </div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jsmediatags/3.9.7/jsmediatags.min.js"></script>
<script>
    const dom = {
        playlist: document.getElementById('playlist'),
        lyrics: document.getElementById('lyrics'),
        audioPlayer: document.getElementById('audioPlayer'),
        themeToggle: document.getElementById('checkbox'),
        loadOnlineBtn: document.getElementById('loadOnlineBtn'),
        fileInput: document.getElementById('fileInput'),
        fileDropArea: document.getElementById('fileDropArea'),
        showPlaylistBtn: document.getElementById('showPlaylistBtn'),
        showLyricsBtn: document.getElementById('showLyricsBtn'),
    };

    const state = {
        isOnlineMode: false,
        audioFiles: [],
        lyricsFiles: {},
        onlineSongs: [],
        currentTrackIndex: -1,
        currentAudioUrl: null,
        lyricsData: [],
        currentLyricLine: -1,
    };

    const API_BASE_URL = 'https://*****.****.xyz';
    const RADAR_IDS = ['3136952023', '5300458264', '5320167908', '5362359247'];

    window.addEventListener('load', setupInteractions);
    dom.audioPlayer.addEventListener('ended', autoPlayNext);
    dom.audioPlayer.addEventListener('timeupdate', syncLyrics);

    function setupInteractions() {
        const savedTheme = localStorage.getItem('theme');
        if (savedTheme) {
            document.body.classList.toggle('dark-mode', savedTheme === 'dark');
            dom.themeToggle.checked = savedTheme === 'dark';
        }
        dom.themeToggle.addEventListener('change', (e) => {
            const isDark = e.target.checked;
            document.body.classList.toggle('dark-mode', isDark);
            localStorage.setItem('theme', isDark ? 'dark' : 'light');
        });

        dom.loadOnlineBtn.addEventListener('click', handleOnlineMode);
        dom.fileInput.addEventListener('change', (e) => processFiles(e.target.files));
        dom.fileDropArea.addEventListener('dragover', (e) => { e.preventDefault(); e.currentTarget.classList.add('dragover'); });
        dom.fileDropArea.addEventListener('dragleave', (e) => e.currentTarget.classList.remove('dragover'));
        dom.fileDropArea.addEventListener('drop', (e) => {
            e.preventDefault();
            e.currentTarget.classList.remove('dragover');
            processFiles(e.dataTransfer.files);
        });
        
        dom.showPlaylistBtn.addEventListener('click', () => switchMobileView('playlist'));
        dom.showLyricsBtn.addEventListener('click', () => switchMobileView('lyrics'));
    }

    function switchMobileView(view) {
        if (window.innerWidth > 768) return;
        if (view === 'playlist') {
            dom.showPlaylistBtn.classList.add('active');
            dom.showLyricsBtn.classList.remove('active');
            dom.playlist.classList.remove('mobile-hidden');
            dom.lyrics.classList.add('mobile-hidden');
        } else {
            dom.showPlaylistBtn.classList.remove('active');
            dom.showLyricsBtn.classList.add('active');
            dom.playlist.classList.add('mobile-hidden');
            dom.lyrics.classList.remove('mobile-hidden');
        }
    }
    
    function playPrevious() {
        if (state.currentTrackIndex <= 0) return;
        const newIndex = state.currentTrackIndex - 1;
        state.isOnlineMode ? findAndPlayOnlineSong(newIndex) : playAudio(newIndex);
    }

    function playNext() {
        const limit = state.isOnlineMode ? state.onlineSongs.length : state.audioFiles.length;
        if (state.currentTrackIndex >= limit - 1) {
            dom.lyrics.innerHTML = '<div>已是列表最后一首。</div>';
            return;
        }
        const newIndex = state.currentTrackIndex + 1;
        state.isOnlineMode ? findAndPlayOnlineSong(newIndex) : playAudio(newIndex);
    }

    function autoPlayNext() { playNext(); }
    
    function processFiles(files) {
        if (files.length === 0) return;
        state.isOnlineMode = false;
        if (window.innerWidth <= 768) switchMobileView('playlist');
        let newFilesAdded = false;
        for (const file of files) {
            const ext = file.name.split('.').pop().toLowerCase();
            const baseName = file.name.split('.').slice(0, -1).join('.');
            if (['mp3', 'wav', 'ogg', 'm4a', 'flac', 'aac', 'wma'].includes(ext)) {
                if (!state.audioFiles.some(f => f.name === file.name)) {
                    state.audioFiles.push(file);
                    newFilesAdded = true;
                }
            } else if (['lrc', 'txt'].includes(ext)) {
                state.lyricsFiles[baseName] = file;
            }
        }
        if (newFilesAdded) renderLocalPlaylist();
        if (state.audioFiles.length > 0 && dom.audioPlayer.paused) playAudio(0);
    }

    function playAudio(index) {
        if (index < 0 || index >= state.audioFiles.length) return;
        if (state.currentAudioUrl) URL.revokeObjectURL(state.currentAudioUrl);
        
        state.isOnlineMode = false;
        state.currentTrackIndex = index;
        updatePlaylistHighlight();
        
        const audioFile = state.audioFiles[index];
        state.currentAudioUrl = URL.createObjectURL(audioFile);
        dom.audioPlayer.src = state.currentAudioUrl;
        dom.audioPlayer.load();
        dom.audioPlayer.play();
        
        resetLyrics();
        loadLyricsForFile(audioFile);
    }

    async function handleOnlineMode() {
        state.isOnlineMode = true;
        if (window.innerWidth <= 768) switchMobileView('playlist');

        const btnText = dom.loadOnlineBtn.querySelector('.btn-text');
        const loader = dom.loadOnlineBtn.querySelector('.loader');
        
        dom.loadOnlineBtn.disabled = true;
        btnText.style.display = 'none';
        loader.style.display = 'inline-block';
        
        try {
            await fetchAndProcessRadars();
            if (state.onlineSongs.length > 0) findAndPlayOnlineSong(0);
        } catch (error) {
            console.error("加载在线歌曲时发生最终错误:", error);
            dom.playlist.innerHTML = "<div>加载失败,请稍后再试。</div>";
        } finally {
            dom.loadOnlineBtn.disabled = false;
            btnText.style.display = 'inline-block';
            loader.style.display = 'none';
        }
    }

    async function fetchAndProcessRadars() {
        dom.playlist.innerHTML = `<div>正在聚合四大雷达歌单...</div>`;
        const promises = RADAR_IDS.map(id => 
            fetch(`${API_BASE_URL}/playlist/detail?id=${id}`)
            .then(res => res.ok ? res.json() : Promise.reject(`歌单${id}加载失败`))
        );
        const results = await Promise.allSettled(promises);
        
        let allTrackIds = [];
        results.forEach(result => {
            if (result.status === 'fulfilled' && result.value.playlist?.trackIds) {
                allTrackIds.push(...result.value.playlist.trackIds);
            }
        });

        const uniqueSongsMap = new Map();
        const batchSize = 200;
        for (let i = 0; i < allTrackIds.length; i += batchSize) {
            const idsString = allTrackIds.slice(i, i + batchSize).map(item => item.id).join(',');
            try {
                const songsRes = await fetch(`${API_BASE_URL}/song/detail?ids=${idsString}`);
                if (songsRes.ok) {
                    const songsData = await songsRes.json();
                    songsData.songs?.forEach(song => uniqueSongsMap.set(song.id, song));
                }
            } catch (e) {
                console.warn("分批获取歌曲详情失败", e);
            }
        }

        if (uniqueSongsMap.size === 0) throw new Error("未能获取任何歌曲详情");
        
        let finalSongs = Array.from(uniqueSongsMap.values());
        shuffleArray(finalSongs);
        state.onlineSongs = finalSongs;
        renderOnlinePlaylist();
    }

    async function findAndPlayOnlineSong(startIndex) {
        for (let i = startIndex; i < state.onlineSongs.length; i++) {
            try {
                await playOnlineSong(i);
                return; // 成功播放后退出循环
            } catch (error) {
                console.log(`第 ${i + 1} 首 (${state.onlineSongs[i].name}) 尝试失败,继续...`);
            }
        }
        dom.lyrics.innerHTML = '<div>抱歉,当前列表中的歌曲均无法播放。</div>';
    }

    async function playOnlineSong(index) {
        state.isOnlineMode = true;
        state.currentTrackIndex = index;
        updatePlaylistHighlight();
        resetLyrics();

        const song = state.onlineSongs[index];
        dom.lyrics.innerHTML = `<div>正在加载: ${song.name}</div>`;
        
        const urlRes = await fetch(`${API_BASE_URL}/song/url?id=${song.id}&br=320000`);
        if (!urlRes.ok) throw new Error(`获取播放地址失败`);
        const urlData = await urlRes.json();
        if (!urlData.data?.[0]?.url) throw new Error('无效的播放地址');

        dom.audioPlayer.src = urlData.data[0].url.replace(/^http:/, 'https:');
        dom.audioPlayer.load();
        dom.audioPlayer.play();

        const lyricRes = await fetch(`${API_BASE_URL}/lyric?id=${song.id}`);
        if (lyricRes.ok) {
            const lyricData = await lyricRes.json();
            if (lyricData.lrc?.lyric) {
                displayLyrics(lyricData.lrc.lyric);
            } else {
                dom.lyrics.innerHTML = '<div>未找到在线歌词。</div>';
            }
        } else {
            dom.lyrics.innerHTML = '<div>歌词加载失败。</div>';
        }
    }

    function renderLocalPlaylist() {
        dom.playlist.innerHTML = state.audioFiles.map((f, i) => `<div id="track-item-${i}" onclick="playAudio(${i})">${f.name}</div>`).join('') || "<div>请选择本地音乐</div>";
    }

    function renderOnlinePlaylist() {
        dom.playlist.innerHTML = state.onlineSongs.map((s, i) => `<div id="track-item-${i}" onclick="findAndPlayOnlineSong(${i})">${s.name} - ${s.ar.map(a => a.name).join('/')}</div>`).join('') || "<div>列表为空</div>";
    }

    function updatePlaylistHighlight() {
        const items = dom.playlist.querySelectorAll('div');
        let currentItem = null;
        items.forEach((item, index) => {
            const isCurrent = index === state.currentTrackIndex;
            item.classList.toggle('current', isCurrent);
            if (isCurrent) currentItem = item;
        });
        if (currentItem) {
            currentItem.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
        }
    }

    function resetLyrics() {
        state.lyricsData = [];
        state.currentLyricLine = -1;
        dom.lyrics.innerHTML = '<div>...</div>';
    }

    async function loadLyricsForFile(audioFile) {
        const baseName = audioFile.name.split('.').slice(0, -1).join('.');
        const lyricsFile = state.lyricsFiles[baseName];
        if (lyricsFile) {
            const reader = new FileReader();
            reader.onload = (e) => displayLyrics(e.target.result);
            reader.readAsText(lyricsFile);
        } else {
            try {
                const searchRes = await fetch(`${API_BASE_URL}/search?keywords=${encodeURIComponent(baseName)}`);
                const searchData = await searchRes.json();
                const songId = searchData.result?.songs?.[0]?.id;
                if (songId) {
                    const lyricRes = await fetch(`${API_BASE_URL}/lyric?id=${songId}`);
                    const lyricData = await lyricRes.json();
                    if (lyricData.lrc?.lyric) {
                        displayLyrics(lyricData.lrc.lyric);
                        return;
                    }
                }
                readEmbeddedLyrics(audioFile); // Fallback
            } catch (error) {
                readEmbeddedLyrics(audioFile); // Fallback on error
            }
        }
    }

    function readEmbeddedLyrics(file) {
        jsmediatags.read(file, {
            onSuccess: (tag) => {
                const lyricsText = tag.tags.USLT?.data ?? tag.tags.SYLT?.data ?? tag.tags.lyrics;
                if (lyricsText) {
                    displayLyrics(lyricsText);
                } else {
                    dom.lyrics.innerHTML = '<div>未找到任何歌词。</div>';
                }
            },
            onError: () => {
                dom.lyrics.innerHTML = '<div>读取内嵌歌词失败。</div>';
            }
        });
    }
    
    function parseLrc(text) {
        if (!text) return [];
        const lines = text.split('\n');
        const result = [];
        // 改进后的正则,兼容更多格式
        const timeRegex = /\[(\d{2}):(\d{2})[.:](\d{2,3})\]/g;
        for (const line of lines) {
            const textContent = line.replace(timeRegex, '').trim();
            if (textContent) {
                let match;
                // 重置正则索引,确保可以匹配一行内的多个时间标签
                timeRegex.lastIndex = 0; 
                while ((match = timeRegex.exec(line)) !== null) {
                    const minutes = parseInt(match[1], 10);
                    const seconds = parseInt(match[2], 10);
                    const milliseconds = parseInt(match[3].padEnd(3, '0'), 10);
                    result.push({
                        time: minutes * 60 + seconds + milliseconds / 1000,
                        text: textContent
                    });
                }
            }
        }
        return result.sort((a, b) => a.time - b.time);
    }


    function displayLyrics(lrcText) {
        state.lyricsData = parseLrc(lrcText);
        if (state.lyricsData.length > 0) {
            dom.lyrics.innerHTML = state.lyricsData.map((item, index) => `<div id="lyric-line-${index}">${item.text}</div>`).join('');
        } else {
            // 如果解析后没内容,则原文显示
            dom.lyrics.innerHTML = lrcText.split('\n').map(line => `<div>${line || '&nbsp;'}</div>`).join('');
        }
        state.currentLyricLine = -1;
        syncLyrics();
    }

    function syncLyrics() {
        if (state.lyricsData.length === 0 || dom.audioPlayer.paused) return;

        const currentTime = dom.audioPlayer.currentTime;
        let newIndex = state.lyricsData.findIndex((line, i) => {
            const nextLine = state.lyricsData[i + 1];
            return currentTime >= line.time && (nextLine ? currentTime < nextLine.time : true);
        });

        if (newIndex !== -1 && newIndex !== state.currentLyricLine) {
            // [优化-4] 移除强制切换视图的逻辑
            // if (window.innerWidth <= 768 && dom.lyrics.classList.contains('mobile-hidden')) {
            //     switchMobileView('lyrics');
            // }
            
            const prevLine = document.getElementById(`lyric-line-${state.currentLyricLine}`);
            if (prevLine) prevLine.classList.remove('current');

            const currentLine = document.getElementById(`lyric-line-${newIndex}`);
            if (currentLine) {
                currentLine.classList.add('current');
                // 只有当歌词视图可见时才滚动
                if (dom.lyrics.offsetParent !== null) {
                    currentLine.scrollIntoView({ behavior: 'smooth', block: 'center' });
                }
            }
            state.currentLyricLine = newIndex;
        }
    }

    function shuffleArray(array) {
        let currentIndex = array.length, randomIndex;
        while (currentIndex != 0) {
            randomIndex = Math.floor(Math.random() * currentIndex);
            currentIndex--;
            [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
        }
        return array;
    }
</script>
</body>
</html>



3 个赞

感谢分享~

不错欸 :xhj003:

感谢分享,求赞

比较麻烦了 还得自己搭建个api,不如用软件听

挺不错的东西了。现在版权看得紧了,想找免费资源真的是太难了。

这个不错哦

好了,现在我想知道如何自己搭建api