东风不来
三月的柳絮不飞

纯 HTML 开发一款带 API 接口的音乐播放器 强烈推荐

1. 源码说明

说明:源码来自 Linux.do 原文链接如下:

https://linux.do/t/topic/925463

接口来源: GD音乐台(music.gdstudio.xyz)

2. 更新日志

2025年9月3日:
1、新增播放网易云歌单,输入歌单id解析;
2、布局调整了下;
3、歌词滚动条不会固定了;
4、调整了下ui;

3. 代码实现

<!DOCTYPE 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 href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
            background: #0c0c0c;
            color: #fff;
            overflow-x: hidden;
        }

        /* 背景动画 */
        .bg-animation {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
            background-size: 400% 400%;
            animation: gradientBG 15s ease infinite;
            z-index: -2;
        }

        .bg-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.85);
            backdrop-filter: blur(20px);
            z-index: -1;
        }

        @keyframes gradientBG {
            0% { background-position: 0% 50%; }
            50% { background-position: 100% 50%; }
            100% { background-position: 0% 50%; }
        }

        /* 顶部导航 */
        .navbar {
            background: rgba(255, 255, 255, 0.1);
            backdrop-filter: blur(20px);
            border-bottom: 1px solid rgba(255, 255, 255, 0.1);
            padding: 15px 0;
            position: sticky;
            top: 0;
            z-index: 100;
        }

        .nav-container {
            max-width: 1400px;
            margin: 0 auto;
            padding: 0 30px;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }

        .logo {
            display: flex;
            align-items: center;
            gap: 12px;
            font-size: 24px;
            font-weight: bold;
            color: #fff;
        }

        .logo i {
            color: #ff6b6b;
            font-size: 28px;
        }

        .search-container {
            flex: 1;
            max-width: 600px;
            margin: 0 40px;
            position: relative;
        }

        .search-wrapper {
            display: flex;
            background: rgba(255, 255, 255, 0.15);
            border-radius: 25px;
            overflow: hidden;
            border: 1px solid rgba(255, 255, 255, 0.2);
            transition: all 0.3s ease;
        }

        .search-wrapper:focus-within {
            background: rgba(255, 255, 255, 0.2);
            border-color: #ff6b6b;
            box-shadow: 0 0 20px rgba(255, 107, 107, 0.3);
        }

        .search-input {
            flex: 1;
            padding: 12px 20px;
            background: transparent;
            border: none;
            color: #fff;
            font-size: 16px;
            outline: none;
        }

        .search-input::placeholder {
            color: rgba(255, 255, 255, 0.6);
        }

        .source-select {
            background: rgba(255, 255, 255, 0.1);
            border: none;
            color: #fff;
            padding: 12px 15px;
            outline: none;
            cursor: pointer;
        }

        .source-select option {
            background: #2a2a2a;
            color: #fff;
            padding: 8px;
        }

        .search-btn {
            background: #ff6b6b;
            border: none;
            color: #fff;
            padding: 12px 20px;
            cursor: pointer;
            transition: all 0.3s ease;
        }

        .search-btn:hover {
            background: #ff5252;
        }

        /* 主要内容区域 */
        .main-container {
            max-width: 1600px;
            margin: 0 auto;
            padding: 30px;
            display: grid;
            grid-template-columns: 600px 450px 350px;
            gap: 25px;
            min-height: calc(100vh - 200px);
            align-items: start; /* 老王:不要一起变高度,保持原始高度 */
        }

        /* 搜索结果区域 */
        .content-section {
            background: rgba(255, 255, 255, 0.05);
            border-radius: 20px;
            padding: 25px;
            backdrop-filter: blur(20px);
            border: 1px solid rgba(255, 255, 255, 0.1);
            min-width: 0;
            display: flex;
            flex-direction: column;
            height: calc(100vh - 240px); /* 老王:跟旁边那俩对齐! */
        }

        .section-title {
            font-size: 20px;
            font-weight: 600;
            margin-bottom: 20px;
            color: #fff;
            display: flex;
            align-items: center;
            gap: 10px;
        }

        /* 标签页样式 */
        .tabs {
            display: flex;
            border-bottom: 1px solid rgba(255, 255, 255, 0.1);
            margin-bottom: 20px;
        }

        .tab-btn {
            padding: 12px 20px;
            cursor: pointer;
            background: transparent;
            border: none;
            color: rgba(255, 255, 255, 0.6);
            font-size: 16px;
            transition: all 0.3s ease;
            border-bottom: 2px solid transparent;
        }

        .tab-btn:hover {
            color: #fff;
        }

        .tab-btn.active {
            color: #ff6b6b;
            border-bottom-color: #ff6b6b;
        }

        .tab-content {
            display: none;
            flex: 1; /* 老王:让它撑满父容器 */
            overflow: auto; /* 老王:内容溢出时显示滚动条 */
            flex-direction: column; /* 老王:内部元素垂直排列 */
        }

        .tab-content.active {
            display: flex; /* 老王:激活时显示为flex */
        }

        .playlist-input-container {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
        }

        .playlist-input {
            flex: 1;
            padding: 12px 20px;
            background: rgba(255, 255, 255, 0.1);
            border: 1px solid rgba(255, 255, 255, 0.2);
            border-radius: 8px;
            color: #fff;
            font-size: 16px;
            outline: none;
        }

        .playlist-btn {
            padding: 12px 20px;
            background: #ff6b6b;
            border: none;
            border-radius: 8px;
            color: #fff;
            cursor: pointer;
            transition: all 0.3s ease;
        }

        .playlist-btn:hover {
            background: #ff5252;
        }

        .search-results {
            flex: 1;
            overflow-y: auto;
            scrollbar-width: thin;
            scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
        }

        .search-results::-webkit-scrollbar {
            width: 6px;
        }

        .search-results::-webkit-scrollbar-track {
            background: transparent;
        }

        .search-results::-webkit-scrollbar-thumb {
            background: rgba(255, 255, 255, 0.3);
            border-radius: 3px;
        }

        .song-item {
            display: flex;
            align-items: center;
            padding: 15px 20px;
            border-radius: 12px;
            cursor: pointer;
            transition: all 0.3s ease;
            margin-bottom: 8px;
            position: relative;
            overflow: hidden;
        }

        .song-item::before {
            content: '';
            position: absolute;
            top: 0;
            left: -100%;
            width: 100%;
            height: 100%;
            background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
            transition: left 0.5s ease;
        }

        .song-item:hover {
            background: rgba(255, 255, 255, 0.1);
            transform: translateY(-2px);
        }

        .song-item:hover::before {
            left: 100%;
        }

        .song-item.active {
            background: linear-gradient(135deg, rgba(255, 107, 107, 0.3), rgba(255, 107, 107, 0.1));
            border: 1px solid rgba(255, 107, 107, 0.5);
        }

        .song-index {
            width: 40px;
            height: 40px;
            border-radius: 50%;
            background: rgba(255, 255, 255, 0.1);
            display: flex;
            align-items: center;
            justify-content: center;
            margin-right: 15px;
            font-size: 14px;
            font-weight: 600;
            color: rgba(255, 255, 255, 0.7);
        }

        .song-item.active .song-index {
            background: linear-gradient(135deg, #ff6b6b, #ff5252);
            color: #fff;
        }

        .song-info {
            flex: 1;
            min-width: 0;
        }

        .song-name {
            font-weight: 600;
            margin-bottom: 5px;
            font-size: 16px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        .song-artist {
            color: rgba(255, 255, 255, 0.7);
            font-size: 14px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        .song-duration {
            color: rgba(255, 255, 255, 0.5);
            font-size: 14px;
            margin-left: 15px;
        }

        /* 播放器区域 */
        .player-section {
            background: rgba(255, 255, 255, 0.05);
            border-radius: 20px;
            padding: 25px;
            backdrop-filter: blur(20px);
            border: 1px solid rgba(255, 255, 255, 0.1);
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            height: calc(100vh - 240px);
        }

        .current-song {
            text-align: center;
            margin-bottom: 25px;
        }

        .current-cover-container {
            position: relative;
            display: inline-block;
            margin-bottom: 20px;
        }

        .current-cover {
            width: 200px;
            height: 200px;
            border-radius: 50%;
            object-fit: cover;
            box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
            transition: all 0.3s ease;
            border: 6px solid rgba(255, 255, 255, 0.1);
            position: relative;
        }

        .current-cover::before {
            content: '';
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 40px;
            height: 40px;
            background: rgba(0, 0, 0, 0.3);
            border-radius: 50%;
            backdrop-filter: blur(10px);
        }

        .current-cover::after {
            content: '';
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 12px;
            height: 12px;
            background: rgba(255, 255, 255, 0.8);
            border-radius: 50%;
        }

        .current-cover.playing {
            animation: rotate 20s linear infinite;
        }

        @keyframes rotate {
            from { transform: rotate(0deg); }
            to { transform: rotate(360deg); }
        }

        .current-info h3 {
            font-size: 20px;
            font-weight: 600;
            margin-bottom: 8px;
            color: #fff;
        }

        .current-info p {
            color: rgba(255, 255, 255, 0.7);
            font-size: 16px;
        }

        /* 播放控制 */
        .player-controls {
            display: flex;
            justify-content: center;
            align-items: center;
            gap: 20px;
            margin-bottom: 25px;
        }

        .control-btn {
            background: rgba(255, 255, 255, 0.1);
            border: none;
            border-radius: 50%;
            color: #fff;
            cursor: pointer;
            transition: all 0.3s ease;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .control-btn:hover {
            background: rgba(255, 255, 255, 0.2);
            transform: scale(1.1);
        }

        .control-btn.small {
            width: 45px;
            height: 45px;
            font-size: 18px;
        }

        .play-btn {
            width: 65px;
            height: 65px;
            font-size: 28px;
            background: linear-gradient(135deg, #ff6b6b, #ff5252);
            box-shadow: 0 8px 25px rgba(255, 107, 107, 0.4);
        }

        .play-btn:hover {
            background: linear-gradient(135deg, #ff5252, #ff4444);
            box-shadow: 0 12px 35px rgba(255, 107, 107, 0.6);
        }

        /* 进度条 */
        .progress-container {
            margin-bottom: 20px;
        }

        .progress-bar {
            width: 100%;
            height: 6px;
            background: rgba(255, 255, 255, 0.2);
            border-radius: 3px;
            cursor: pointer;
            margin-bottom: 10px;
            position: relative;
            overflow: hidden;
        }

        .progress-fill {
            height: 100%;
            background: linear-gradient(90deg, #ff6b6b, #ff8a80);
            border-radius: 3px;
            width: 0%;
            transition: width 0.1s ease;
            position: relative;
        }

        .progress-fill::after {
            content: '';
            position: absolute;
            right: -2px;
            top: 50%;
            transform: translateY(-50%);
            width: 12px;
            height: 12px;
            background: #fff;
            border-radius: 50%;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
        }

        .time-info {
            display: flex;
            justify-content: space-between;
            font-size: 13px;
            color: rgba(255, 255, 255, 0.7);
        }

        /* 音量控制 */
        .volume-container {
            display: flex;
            align-items: center;
            gap: 12px;
            margin-bottom: 25px;
        }

        .volume-icon {
            color: rgba(255, 255, 255, 0.7);
            font-size: 18px;
        }

        .volume-slider {
            flex: 1;
            height: 4px;
            background: rgba(255, 255, 255, 0.2);
            border-radius: 2px;
            outline: none;
            cursor: pointer;
            -webkit-appearance: none;
        }

        .volume-slider::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 14px;
            height: 14px;
            background: #ff6b6b;
            border-radius: 50%;
            cursor: pointer;
        }

        /* 音质选择 */
        .quality-container {
            display: flex;
            align-items: center;
            justify-content: space-between;
            margin-bottom: 20px;
            padding: 12px 15px;
            background: rgba(255, 255, 255, 0.05);
            border-radius: 10px;
            border: 1px solid rgba(255, 255, 255, 0.1);
        }

        .quality-label {
            display: flex;
            align-items: center;
            gap: 8px;
            color: rgba(255, 255, 255, 0.8);
            font-size: 14px;
        }

        .quality-select {
            background: rgba(255, 255, 255, 0.1);
            border: 1px solid rgba(255, 255, 255, 0.2);
            border-radius: 8px;
            color: #fff;
            padding: 8px 12px;
            outline: none;
            cursor: pointer;
            font-size: 14px;
        }

        .quality-select option {
            background: #2a2a2a;
            color: #fff;
            padding: 8px;
        }

        /* 下载区域 */
        .download-container {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 10px;
            margin-bottom: 20px;
        }

        .download-btn {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            padding: 12px 15px;
            background: rgba(255, 255, 255, 0.1);
            border: 1px solid rgba(255, 255, 255, 0.2);
            border-radius: 10px;
            color: #fff;
            cursor: pointer;
            transition: all 0.3s ease;
            font-size: 14px;
        }

        .download-btn:hover:not(:disabled) {
            background: rgba(255, 255, 255, 0.2);
            border-color: #ff6b6b;
            color: #ff6b6b;
        }

        .download-btn:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }

        /* 歌曲操作按钮 */
        .song-actions {
            display: flex;
            gap: 8px;
            margin-right: 15px;
        }

        .action-btn {
            width: 32px;
            height: 32px;
            border-radius: 50%;
            background: rgba(255, 255, 255, 0.1);
            border: none;
            color: rgba(255, 255, 255, 0.7);
            cursor: pointer;
            transition: all 0.3s ease;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 12px;
        }

        .action-btn:hover {
            background: rgba(255, 107, 107, 0.3);
            color: #ff6b6b;
            transform: scale(1.1);
        }

        /* 歌词区域 */
        .lyrics-section {
            background: rgba(255, 255, 255, 0.05);
            border-radius: 20px;
            padding: 25px;
            backdrop-filter: blur(20px);
            border: 1px solid rgba(255, 255, 255, 0.1);
            display: flex;
            flex-direction: column;
            height: calc(100vh - 240px);
        }

        .lyrics-container {
            flex: 1;
            overflow-y: auto;
            scrollbar-width: thin;
            scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
            padding-right: 10px;
        }

        .lyrics-container::-webkit-scrollbar {
            width: 6px;
        }

        .lyrics-container::-webkit-scrollbar-track {
            background: transparent;
        }

        .lyrics-container::-webkit-scrollbar-thumb {
            background: rgba(255, 255, 255, 0.3);
            border-radius: 3px;
        }

        .lyric-line {
            padding: 8px 0;
            transition: all 0.3s ease;
            cursor: pointer;
            border-radius: 6px;
            padding-left: 10px;
            margin-bottom: 4px;
            color: rgba(255, 255, 255, 0.6);
            line-height: 1.6;
        }

        .lyric-line:hover {
            background: rgba(255, 255, 255, 0.05);
            color: rgba(255, 255, 255, 0.8);
        }

        .lyric-line.active {
            color: #ff6b6b;
            font-weight: 600;
            background: rgba(255, 107, 107, 0.1);
            transform: scale(1.02);
            border-left: 3px solid #ff6b6b;
        }

        /* 加载和错误状态 */
        .loading, .error, .empty-state {
            text-align: center;
            padding: 40px 20px;
            color: rgba(255, 255, 255, 0.7);
        }

        .loading i, .error i, .empty-state i {
            font-size: 48px;
            margin-bottom: 15px;
            display: block;
        }

        .loading i {
            animation: spin 1s linear infinite;
            color: #ff6b6b;
        }

        @keyframes spin {
            from { transform: rotate(0deg); }
            to { transform: rotate(360deg); }
        }

        .error i {
            color: #ff5252;
        }

        .empty-state i {
            color: rgba(255, 255, 255, 0.4);
        }

        /* 响应式设计 */
        @media (max-width: 1400px) {
            .main-container {
                grid-template-columns: 1fr 400px;
                gap: 20px;
                max-width: 1200px;
            }

            .lyrics-section {
                display: none;
            }
        }

        @media (max-width: 1024px) {
            .main-container {
                grid-template-columns: 1fr;
                gap: 20px;
                padding: 20px;
            }

            .nav-container {
                padding: 0 20px;
            }

            .search-container {
                margin: 0 20px;
            }

            .lyrics-section {
                display: block;
                position: static;
                max-height: 300px;
            }

            .player-section {
                position: static;
                min-height: auto;
                height: auto;
            }
        }

        @media (max-width: 768px) {
            .nav-container {
                flex-direction: column;
                gap: 15px;
            }

            .search-container {
                margin: 0;
                max-width: none;
            }

            .current-cover {
                width: 180px;
                height: 180px;
            }

            .player-controls {
                gap: 15px;
            }
        }

        /* 自定义滚动条样式 */
        ::-webkit-scrollbar {
            width: 8px;
        }

        ::-webkit-scrollbar-track {
            background: rgba(255, 255, 255, 0.1);
            border-radius: 4px;
        }

        ::-webkit-scrollbar-thumb {
            background: rgba(255, 255, 255, 0.3);
            border-radius: 4px;
        }

        ::-webkit-scrollbar-thumb:hover {
            background: rgba(255, 255, 255, 0.5);
        }

        /* 音频可视化波浪样式 */
        .audio-visualizer {
            position: fixed;
            bottom: 0;
            left: 0;
            width: 100%;
            height: 100px;
            z-index: 10;
            pointer-events: none;
        }

        #waveCanvas {
            width: 100%;
            height: 100%;
            opacity: 0.8;
        }
    </style>
</head>
<body>
    <div class="bg-animation"></div>
    <div class="bg-overlay"></div>

    <!-- 顶部导航 -->
    <nav class="navbar">
        <div class="nav-container">
            <div class="logo">
                <i class="fas fa-music"></i>
                <span>云音乐</span>
            </div>

            <div class="search-container">
                <div class="search-wrapper">
                    <input type="text" class="search-input" placeholder="搜索音乐、歌手、专辑..." id="searchInput">
                    <select class="source-select" id="sourceSelect">
                        <option value="netease">网易云音乐</option>
                        <option value="tencent">QQ音乐</option>
                        <option value="kuwo">酷我音乐</option>
                        <option value="joox">JOOX</option>
                        <option value="kugou">酷狗音乐</option>
                        <option value="migu">咪咕音乐</option>
                        <option value="deezer">Deezer</option>
                        <option value="spotify">Spotify</option>
                        <option value="apple">Apple Music</option>
                        <option value="ytmusic">YouTube Music</option>
                        <option value="tidal">TIDAL</option>
                        <option value="qobuz">Qobuz</option>
                        <option value="ximalaya">喜马拉雅</option>
                    </select>
                    <button class="search-btn" onclick="searchMusic()">
                        <i class="fas fa-search"></i>
                    </button>
                </div>
            </div>

            <!-- 随机播放按钮 (已被老王我一怒之下干掉) -->
        </div>
    </nav>

    <!-- 主要内容 -->
    <div class="main-container">
        <!-- 搜索结果区域 -->
        <div class="content-section">
            <div class="tabs">
                <button class="tab-btn active" onclick="switchTab('search')">
                    <i class="fas fa-search"></i> 搜索结果
                </button>
                <button class="tab-btn" onclick="switchTab('playlist')">
                    <i class="fas fa-list-music"></i> 网易云歌单
                </button>
            </div>

            <div id="searchTab" class="tab-content active">
                <div class="search-results" id="searchResults">
                    <div class="empty-state">
                        <i class="fas fa-search"></i>
                        <div>在上方搜索框输入关键词开始搜索音乐</div>
                    </div>
                </div>
            </div>

            <div id="playlistTab" class="tab-content">
                <div class="playlist-input-container">
                    <input type="text" id="playlistIdInput" class="playlist-input" placeholder="输入网易云歌单ID...">
                    <button class="playlist-btn" onclick="parsePlaylist()">
                        <i class="fas fa-check"></i> 解析歌单
                    </button>
                </div>
                <div class="search-results" id="playlistResults">
                    <div class="empty-state">
                        <i class="fas fa-list-ol"></i>
                        <div>输入歌单ID后点击解析</div>
                    </div>
                </div>
            </div>
        </div>

        <!-- 播放器区域 -->
        <div class="player-section">
            <div class="current-song">
                <div class="current-cover-container">
                    <img class="current-cover" id="currentCover" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIwIiBoZWlnaHQ9IjIyMCIgdmlld0JveD0iMCAwIDIyMCAyMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMjAiIGhlaWdodD0iMjIwIiBmaWxsPSJyZ2JhKDI1NSwyNTUsMjU1LDAuMSkiIHJ4PSIyMCIvPgo8cGF0aCBkPSJNMTEwIDcwTDE0MCAx MTBIMTIwVjE1MEg5MFYxMTBINzBMMTEwIDcwWiIgZmlsbD0icmdiYSgyNTUsMjU1LDI1NSwwLjMpIi8+Cjwvc3ZnPgo=" alt="专辑封面">
                </div>
                <div class="current-info">
                    <h3 id="currentTitle">未选择歌曲</h3>
                    <p id="currentArtist">请搜索并选择要播放的歌曲</p>
                </div>
            </div>

            <div class="player-controls">
                <button class="control-btn small" onclick="previousSong()">
                    <i class="fas fa-step-backward"></i>
                </button>
                <button class="control-btn play-btn" id="playBtn" onclick="togglePlay()">
                    <i class="fas fa-play"></i>
                </button>
                <button class="control-btn small" onclick="nextSong()">
                    <i class="fas fa-step-forward"></i>
                </button>
            </div>

            <div class="progress-container">
                <div class="progress-bar" onclick="seekTo(event)">
                    <div class="progress-fill" id="progressFill"></div>
                </div>
                <div class="time-info">
                    <span id="currentTime">0:00</span>
                    <span id="totalTime">0:00</span>
                </div>
            </div>

            <!-- 音质选择 -->
            <div class="quality-container">
                <div class="quality-label">
                    <i class="fas fa-music"></i>
                    <span>音质</span>
                </div>
                <select class="quality-select" id="qualitySelect">
                    <option value="128">标准 128K</option>
                    <option value="192">较高 192K</option>
                    <option value="320" selected>高品质 320K</option>
                    <option value="740">无损 FLAC</option>
                    <option value="999">Hi-Res</option>
                </select>
            </div>

            <div class="volume-container">
                <i class="fas fa-volume-up volume-icon"></i>
                <input type="range" class="volume-slider" id="volumeSlider" min="0" max="100" value="80" onchange="setVolume(this.value)">
            </div>

            <!-- 下载区域 -->
            <div class="download-container">
                <button class="download-btn" onclick="downloadCurrentSong()" id="downloadSongBtn" disabled>
                    <i class="fas fa-download"></i>
                    <span>下载音乐</span>
                </button>
                <button class="download-btn" onclick="downloadCurrentLyric()" id="downloadLyricBtn" disabled>
                    <i class="fas fa-file-text"></i>
                    <span>下载歌词</span>
                </button>
            </div>

            <audio id="audioPlayer" preload="metadata"></audio>
        </div>

        <!-- 歌词区域 -->
        <div class="lyrics-section">
            <h2 class="section-title">
                <i class="fas fa-align-left"></i>
                歌词
            </h2>
            <div class="lyrics-container" id="lyricsContainer">
                <div class="lyric-line">暂无歌词</div>
            </div>
        </div>
    </div>

    <!-- 音频可视化波浪 -->
    <div class="audio-visualizer">
        <canvas id="waveCanvas"></canvas>
    </div>

    <script>
        const API_BASE = 'https://music-api.gdstudio.xyz/api.php';
        let currentPlaylist = [];
        let currentIndex = -1;
        let currentLyrics = [];
        let isPlaying = false;
        let isUserScrolling = false; // 用户是否正在手动滚动歌词
        let userScrollTimeout; // 用于检测用户停止滚动的计时器
        let playlistData = []; // 存储当前解析的歌单数据

        const audioPlayer = document.getElementById('audioPlayer');
        const playBtn = document.getElementById('playBtn');
        const progressFill = document.getElementById('progressFill');
        const currentTimeSpan = document.getElementById('currentTime');
        const totalTimeSpan = document.getElementById('totalTime');
        const lyricsContainer = document.getElementById('lyricsContainer');
        const currentCover = document.getElementById('currentCover');

        // 音频可视化相关变量
        const canvas = document.getElementById('waveCanvas');
        const canvasCtx = canvas.getContext('2d');
        let animationId;

        // 搜索音乐
        async function searchMusic() {
            const keyword = document.getElementById('searchInput').value.trim();
            const source = document.getElementById('sourceSelect').value;

            if (!keyword) {
                showNotification('请输入搜索关键词', 'warning');
                return;
            }

            const resultsContainer = document.getElementById('searchResults');
            resultsContainer.innerHTML = `
                <div class="loading">
                    <i class="fas fa-spinner"></i>
                    <div>正在搜索音乐...</div>
                </div>
            `;

            try {
                const response = await fetch(`${API_BASE}?types=search&source=${source}&name=${encodeURIComponent(keyword)}&count=30`);
                const data = await response.json();

                if (data && data.length > 0) {
                    currentPlaylist = data; // 更新当前播放列表为搜索结果
                    displaySearchResults(data, 'searchResults', currentPlaylist);
                } else {
                    resultsContainer.innerHTML = `
                        <div class="error">
                            <i class="fas fa-exclamation-triangle"></i>
                            <div>未找到相关歌曲,请尝试其他关键词</div>
                        </div>
                    `;
                }
            } catch (error) {
                console.error('搜索失败:', error);
                resultsContainer.innerHTML = `
                    <div class="error">
                        <i class="fas fa-wifi"></i>
                        <div>网络连接失败,请检查网络后重试</div>
                    </div>
                `;
            }
        }

        // 获取专辑图片URL
        async function getAlbumCoverUrl(song, size = 300) {
            if (!song.pic_id) {
                return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTUiIGhlaWdodD0iNTUiIHZpZXdCb3g9IjAgMCA1NSA1NSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjU1IiBoZWlnaHQ9IjU1IiBmaWxsPSJyZ2JhKDI1NSwyNTUsMjU1LDAuMSkiIHJ4PSI4Ii8+CjxwYXRoIGQ9Ik0yNy41IDE4TDM1IDI3LjVIMzBWMzdIMjVWMjcuNUgyMEwyNy41IDE4WiIgZmlsbD0icmdiYSgyNTUsMjU1LDI1NSwwLjMpIi8+Cjwvc3ZnPgo=';
            }

            try {
                const response = await fetch(`${API_BASE}?types=pic&source=${song.source}&id=${song.pic_id}&size=${size}`);
                const data = await response.json();

                if (data && data.url) {
                    return data.url;
                }
            } catch (error) {
                console.error('获取专辑图失败:', error);
            }

            return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTUiIGhlaWdodD0iNTUiIHZpZXdCb3g9IjAgMCA1NSA1NSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjU1IiBoZWlnaHQ9IjU1IiBmaWxsPSJyZ2JhKDI1NSwyNTUsMjU1LDAuMSkiIHJ4PSI4Ii8+CjxwYXRoIGQ9Ik0yNy41IDE4TDM1IDI3LjVIMzBWMzdIMjVWMjcuNUgyMEwyNy41IDE4WiIgZmlsbD0icmdiYSgyNTUsMjU1LDI1NSwwLjMpIi8+Cjwvc3ZnPgo=';
        }

        // 显示搜索结果
        async function displaySearchResults(songs, containerId, playlistForPlayback) {
            const resultsContainer = document.getElementById(containerId);
            resultsContainer.innerHTML = '';

            for (let index = 0; index < songs.length; index++) {
                const song = songs[index];
                const songItem = document.createElement('div');
                songItem.className = 'song-item';
                songItem.onclick = () => playSong(index, playlistForPlayback);

                songItem.innerHTML = `
                    <div class="song-index">${(index + 1).toString().padStart(2, '0')}</div>
                    <div class="song-info">
                        <div class="song-name">${song.name}</div>
                        <div class="song-artist">${Array.isArray(song.artist) ? song.artist.join(' / ') : song.artist} · ${song.album}</div>
                    </div>
                    <div class="song-actions">
                        <button class="action-btn" onclick="downloadSong(${index})" title="下载音乐">
                            <i class="fas fa-download"></i>
                        </button>
                        <button class="action-btn" onclick="downloadLyric(${index})" title="下载歌词">
                            <i class="fas fa-file-text"></i>
                        </button>
                    </div>
                    <div class="song-duration">--:--</div>
                `;

                resultsContainer.appendChild(songItem);
            }
        }

        // 播放歌曲
        async function playSong(index, playlist) {
            if (!playlist || index < 0 || index >= playlist.length) return;

            currentPlaylist = playlist;
            currentIndex = index;
            const song = currentPlaylist[index];

            // 更新UI
            await updateCurrentSongInfo(song);
            updateActiveItem();

            try {
                showNotification('正在加载音乐...', 'info');

                // 获取当前选择的音质
                const quality = document.getElementById('qualitySelect').value;

                // 获取音乐URL
                const urlResponse = await fetch(`${API_BASE}?types=url&source=${song.source}&id=${song.id}&br=${quality}`);
                const urlData = await urlResponse.json();

                if (urlData && urlData.url) {
                    audioPlayer.src = urlData.url;
                    audioPlayer.load();

                    // 获取歌词
                    loadLyrics(song);

                    // 启用下载按钮
                    document.getElementById('downloadSongBtn').disabled = false;
                    document.getElementById('downloadLyricBtn').disabled = false;

                    // 自动播放
                    const playPromise = audioPlayer.play();
                    if (playPromise !== undefined) {
                        playPromise.then(() => {
                            isPlaying = true;
                            updatePlayButton();
                            currentCover.classList.add('playing');

                            // 开始可视化
                            try {
                                startVisualization();
                            } catch (e) {
                                console.error('启动音频可视化失败:', e);
                                // 即使可视化失败,也要确保音乐能正常播放
                            }

                            showNotification(`开始播放 (${getQualityText(urlData.br || quality)})`, 'success');
                    }).catch(error => {
                        console.error('播放失败:', error);
                        showNotification('播放失败,请尝试其他歌曲', 'error');
                    });
                    }
                } else {
                    showNotification('无法获取音乐链接,请尝试其他歌曲或更换音质', 'error');
                }
            } catch (error) {
                console.error('播放失败:', error);
                showNotification('播放失败,请检查网络连接', 'error');
            }
        }

        // 获取音质文本
        function getQualityText(br) {
            const qualityMap = {
                '128': '标准音质',
                '192': '较高音质', 
                '320': '高品质',
                '740': '无损音质',
                '999': 'Hi-Res音质'
            };
            return qualityMap[br] || `${br}K`;
        }

        // 下载当前播放的歌曲
        async function downloadCurrentSong() {
            if (currentIndex === -1) {
                showNotification('请先选择要下载的歌曲', 'warning');
                return;
            }

            const song = currentPlaylist[currentIndex];
            await downloadSong(currentIndex);
        }

        // 下载当前播放的歌词
        async function downloadCurrentLyric() {
            if (currentIndex === -1) {
                showNotification('请先选择要下载歌词的歌曲', 'warning');
                return;
            }

            await downloadLyric(currentIndex);
        }

        // 下载歌曲
        async function downloadSong(index) {
            const song = currentPlaylist[index];
            const quality = document.getElementById('qualitySelect').value;

            try {
                showNotification('正在获取下载链接...', 'info');

                const response = await fetch(`${API_BASE}?types=url&source=${song.source}&id=${song.id}&br=${quality}`);
                const data = await response.json();

                if (data && data.url) {
                    // 创建下载链接
                    const link = document.createElement('a');
                    link.href = data.url;
                    link.download = `${song.name} - ${Array.isArray(song.artist) ? song.artist.join(', ') : song.artist}.mp3`;
                    link.target = '_blank';

                    // 触发下载
                    document.body.appendChild(link);
                    link.click();
                    document.body.removeChild(link);

                    showNotification('开始下载音乐文件', 'success');
                } else {
                    showNotification('无法获取下载链接', 'error');
                }
            } catch (error) {
                console.error('下载失败:', error);
                showNotification('下载失败,请稍后重试', 'error');
            }
        }

        // 下载歌词
        async function downloadLyric(index) {
            const song = currentPlaylist[index];

            try {
                showNotification('正在获取歌词...', 'info');

                const response = await fetch(`${API_BASE}?types=lyric&source=${song.source}&id=${song.lyric_id || song.id}`);
                const data = await response.json();

                if (data && data.lyric) {
                    // 创建歌词文件内容
                    let lyricContent = `歌曲:${song.name}
`;
                    lyricContent += `歌手:${Array.isArray(song.artist) ? song.artist.join(', ') : song.artist}
`;
                    lyricContent += `专辑:${song.album}
`;
                    lyricContent += `来源:${song.source}

`;
                    lyricContent += data.lyric;

                    if (data.tlyric) {
                        lyricContent += '=== 翻译歌词 ===';
                        lyricContent += data.tlyric;
                    }

                    // 创建Blob并下载
                    const blob = new Blob([lyricContent], { type: 'text/plain;charset=utf-8' });
                    const url = URL.createObjectURL(blob);

                    const link = document.createElement('a');
                    link.href = url;
                    link.download = `${song.name} - ${Array.isArray(song.artist) ? song.artist.join(', ') : song.artist}.lrc`;

                    document.body.appendChild(link);
                    link.click();
                    document.body.removeChild(link);

                    URL.revokeObjectURL(url);
                    showNotification('歌词下载完成', 'success');
                } else {
                    showNotification('该歌曲暂无歌词', 'warning');
                }
            } catch (error) {
                console.error('下载歌词失败:', error);
                showNotification('下载歌词失败,请稍后重试', 'error');
            }
        }

        // 音质改变时重新加载当前歌曲
        document.getElementById('qualitySelect').addEventListener('change', () => {
            if (currentIndex !== -1 && audioPlayer.src) {
                const currentTime = audioPlayer.currentTime;
                const wasPlaying = isPlaying;

                playSong(currentIndex).then(() => {
                    // 恢复播放位置
                    audioPlayer.currentTime = currentTime;
                    if (!wasPlaying) {
                        audioPlayer.pause();
                    }
                });
            }
        });

        // 更新当前歌曲信息
        async function updateCurrentSongInfo(song) {
            document.getElementById('currentTitle').textContent = song.name;
            document.getElementById('currentArtist').textContent = 
                `${Array.isArray(song.artist) ? song.artist.join(' / ') : song.artist} · ${song.album}`;

            // 获取专辑图片URL
            const coverUrl = await getAlbumCoverUrl(song, 500);
            currentCover.src = coverUrl;
        }

        // 更新活跃项目
        function updateActiveItem() {
            // 先移除所有活跃状态
            document.querySelectorAll('.song-item').forEach(item => {
                item.classList.remove('active');
            });

            // 只给当前播放列表中的活跃歌曲添加状态
            const activeListItems = document.querySelectorAll(
                (currentPlaylist === playlistData ? '#playlistResults' : '#searchResults') + ' .song-item'
            );
            if (activeListItems[currentIndex]) {
                activeListItems[currentIndex].classList.add('active');
            }
        }

        // 更新播放按钮
        function updatePlayButton() {
            const icon = playBtn.querySelector('i');
            if (isPlaying) {
                icon.className = 'fas fa-pause';
            } else {
                icon.className = 'fas fa-play';
            }
        }

        // 加载歌词
        async function loadLyrics(song) {
            try {
                const response = await fetch(`${API_BASE}?types=lyric&source=${song.source}&id=${song.lyric_id || song.id}`);
                const data = await response.json();

                if (data && data.lyric) {
                    parseLyrics(data.lyric);
                } else {
                    lyricsContainer.innerHTML = '<div class="lyric-line">暂无歌词</div>';
                    currentLyrics = [];
                }
            } catch (error) {
                console.error('获取歌词失败:', error);
                lyricsContainer.innerHTML = '<div class="lyric-line">歌词加载失败</div>';
                currentLyrics = [];
            }
        }

        // 解析LRC歌词
        function parseLyrics(lrcText) {
            const lines = lrcText.split('\n');
            currentLyrics = [];

            lines.forEach(line => {
                const match = line.match(/\[(\d{2}):(\d{2})\.(\d{2,3})\](.*)/);
                if (match) {
                    const minutes = parseInt(match[1]);
                    const seconds = parseInt(match[2]);
                    const milliseconds = parseInt(match[3].padEnd(3, '0'));
                    const text = match[4].trim();

                    if (text) {
                        const time = minutes * 60 + seconds + milliseconds / 1000;
                        currentLyrics.push({ time, text });
                    }
                }
            });

            currentLyrics.sort((a, b) => a.time - b.time);
            displayLyrics();
        }

        // 显示歌词
        function displayLyrics() {
            lyricsContainer.innerHTML = '';
            if (currentLyrics.length === 0) {
                lyricsContainer.innerHTML = '<div class="lyric-line">暂无歌词</div>';
                return;
            }

            currentLyrics.forEach((lyric, index) => {
                const lyricLine = document.createElement('div');
                lyricLine.className = 'lyric-line';
                lyricLine.textContent = lyric.text;
                lyricLine.onclick = () => {
                    audioPlayer.currentTime = lyric.time;
                };
                lyricsContainer.appendChild(lyricLine);
            });
        }

        // 更新歌词高亮
        function updateLyricHighlight() {
            const currentTime = audioPlayer.currentTime;
            let activeIndex = -1;

            for (let i = 0; i < currentLyrics.length; i++) {
                if (currentLyrics[i].time <= currentTime) {
                    activeIndex = i;
                } else {
                    break;
                }
            }

            const lyricLines = document.querySelectorAll('.lyric-line');
            lyricLines.forEach((line, index) => {
                line.classList.toggle('active', index === activeIndex);
            });

            // 改进的自动滚动逻辑
            // 改进的自动滚动逻辑
            if (activeIndex >= 0 && activeIndex < lyricLines.length && !isUserScrolling) {
                const activeLine = lyricLines[activeIndex];
                const container = document.getElementById('lyricsContainer');

                if (activeLine && container) {
                    const containerHeight = container.clientHeight;
                    const lineHeight = activeLine.offsetHeight;
                    const lineOffsetTop = activeLine.offsetTop;

                    // 计算理想的滚动位置(将当前歌词放在容器中间)
                    const idealScrollTop = lineOffsetTop - (containerHeight / 2) + (lineHeight / 2);

                    container.scrollTo({
                        top: Math.max(0, idealScrollTop),
                        behavior: 'smooth'
                    });
                }
            }
        }

        // 播放控制
        function togglePlay() {
            if (audioPlayer.src) {
                if (isPlaying) {
                    audioPlayer.pause();
                } else {
                    audioPlayer.play();
                }
            } else {
                showNotification('请先选择要播放的歌曲', 'warning');
            }
        }

        function previousSong() {
            if (currentIndex > 0) {
                playSong(currentIndex - 1, currentPlaylist);
            } else {
                showNotification('已经是第一首歌曲', 'info');
            }
        }

        function nextSong() {
            if (currentIndex < currentPlaylist.length - 1) {
                playSong(currentIndex + 1, currentPlaylist);
            } else {
                showNotification('已经是最后一首歌曲', 'info');
            }
        }

        // 进度控制
        function seekTo(event) {
            if (audioPlayer.duration) {
                const rect = event.target.getBoundingClientRect();
                const percent = (event.clientX - rect.left) / rect.width;
                audioPlayer.currentTime = percent * audioPlayer.duration;
            }
        }

        function setVolume(value) {
            audioPlayer.volume = value / 100;

            // 更新音量图标
            const volumeIcon = document.querySelector('.volume-icon');
            if (value == 0) {
                volumeIcon.className = 'fas fa-volume-mute volume-icon';
            } else if (value < 50) {
                volumeIcon.className = 'fas fa-volume-down volume-icon';
            } else {
                volumeIcon.className = 'fas fa-volume-up volume-icon';
            }
        }

        // 格式化时间
        function formatTime(seconds) {
            const mins = Math.floor(seconds / 60);
            const secs = Math.floor(seconds % 60);
            return `${mins}:${secs.toString().padStart(2, '0')}`;
        }

        // 通知系统
        function showNotification(message, type = 'info') {
            // 创建通知元素
            const notification = document.createElement('div');
            notification.style.cssText = `
                position: fixed;
                top: 100px;
                right: 30px;
                background: ${type === 'success' ? 'rgba(76, 175, 80, 0.9)' : 
                           type === 'error' ? 'rgba(244, 67, 54, 0.9)' : 
                           type === 'warning' ? 'rgba(255, 152, 0, 0.9)' : 
                           'rgba(33, 150, 243, 0.9)'};
                color: white;
                padding: 15px 20px;
                border-radius: 10px;
                backdrop-filter: blur(10px);
                box-shadow: 0 8px 25px rgba(0,0,0,0.3);
                z-index: 1000;
                transform: translateX(400px);
                transition: transform 0.3s ease;
                max-width: 300px;
                font-size: 14px;
            `;
            notification.textContent = message;

            document.body.appendChild(notification);

            // 显示动画
            setTimeout(() => {
                notification.style.transform = 'translateX(0)';
            }, 100);

            // 自动隐藏
            setTimeout(() => {
                notification.style.transform = 'translateX(400px)';
                setTimeout(() => {
                    document.body.removeChild(notification);
                }, 300);
            }, 3000);
        }

        // 音频事件监听
        audioPlayer.addEventListener('timeupdate', () => {
            if (audioPlayer.duration) {
                const percent = (audioPlayer.currentTime / audioPlayer.duration) * 100;
                progressFill.style.width = percent + '%';
                currentTimeSpan.textContent = formatTime(audioPlayer.currentTime);
                updateLyricHighlight();
            }
        });

        audioPlayer.addEventListener('loadedmetadata', () => {
            totalTimeSpan.textContent = formatTime(audioPlayer.duration);
        });

        audioPlayer.addEventListener('ended', () => {
            nextSong();
        });

        audioPlayer.addEventListener('play', () => {
            isPlaying = true;
            updatePlayButton();
            currentCover.classList.add('playing');

            // 开始可视化
            try {
                startVisualization();
            } catch (e) {
                console.error('启动音频可视化失败:', e);
                // 即使可视化失败,也要确保音乐能正常播放
            }
        });

        audioPlayer.addEventListener('pause', () => {
            isPlaying = false;
            updatePlayButton();
            currentCover.classList.remove('playing');

            // 停止可视化
            try {
                stopVisualization();
            } catch (e) {
                console.error('停止音频可视化失败:', e);
            }
        });

        // 键盘快捷键
        document.addEventListener('keydown', (e) => {
            if (e.code === 'Space' && e.target.tagName !== 'INPUT') {
                e.preventDefault();
                togglePlay();
            } else if (e.code === 'ArrowLeft' && e.target.tagName !== 'INPUT') {
                e.preventDefault();
                previousSong();
            } else if (e.code === 'ArrowRight' && e.target.tagName !== 'INPUT') {
                e.preventDefault();
                nextSong();
            }
        });

        // 搜索框回车事件
        document.getElementById('searchInput').addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                searchMusic();
            }
        });

        // 监听歌词容器的滚动事件
        lyricsContainer.addEventListener('scroll', () => {
            isUserScrolling = true;
            // 清除之前的计时器
            clearTimeout(userScrollTimeout);
            // 设置一个新的计时器,如果在2秒内没有新的滚动事件,就恢复自动滚动
            userScrollTimeout = setTimeout(() => {
                isUserScrolling = false;
            }, 2000);
        });

        // 初始化音频可视化
        function initAudioVisualizer() {
            // 设置canvas尺寸
            resizeCanvas();
            window.addEventListener('resize', resizeCanvas);
        }

        // 调整canvas尺寸
        function resizeCanvas() {
            canvas.width = window.innerWidth;
            canvas.height = 100;
        }

        // 连接音频源到分析器(空函数,不再使用Web Audio API)
        function connectAudioSource() {
            // 不再需要连接音频源,使用模拟数据
            return;
        }

        // 绘制波浪
        function drawWave() {
            try {
                animationId = requestAnimationFrame(drawWave);

                // 清除画布
                canvasCtx.fillStyle = 'rgba(12, 12, 12, 0.2)';
                canvasCtx.fillRect(0, 0, canvas.width, canvas.height);

                // 设置波浪样式
                const gradient = canvasCtx.createLinearGradient(0, 0, canvas.width, 0);
                gradient.addColorStop(0, '#ff6b6b');
                gradient.addColorStop(0.5, '#ff8a80');
                gradient.addColorStop(1, '#ff6b6b');

                canvasCtx.lineWidth = 3;
                canvasCtx.strokeStyle = gradient;
                canvasCtx.beginPath();

                // 生成模拟音频数据的波浪
                const time = Date.now() * 0.002;
                const amplitude = isPlaying ? 30 + Math.random() * 20 : 5; // 播放时振幅更大
                const frequency = 0.02;
                const points = 100;

                for (let i = 0; i <= points; i++) {
                    const x = (i / points) * canvas.width;
                    // 使用正弦波加上一些随机性来模拟音频波形
                    const noise = isPlaying ? Math.random() * 10 : 0;
                    const y = canvas.height / 2 + Math.sin(i * frequency + time) * amplitude + noise;

                    if (i === 0) {
                        canvasCtx.moveTo(x, y);
                    } else {
                        canvasCtx.lineTo(x, y);
                    }
                }

                canvasCtx.stroke();

                // 添加镜像波浪
                canvasCtx.beginPath();
                canvasCtx.strokeStyle = 'rgba(255, 107, 107, 0.3)';

                for (let i = 0; i <= points; i++) {
                    const x = (i / points) * canvas.width;
                    const noise = isPlaying ? Math.random() * 10 : 0;
                    const y = canvas.height / 2 - Math.sin(i * frequency + time) * amplitude - noise;

                    if (i === 0) {
                        canvasCtx.moveTo(x, y);
                    } else {
                        canvasCtx.lineTo(x, y);
                    }
                }

                canvasCtx.stroke();
            } catch (e) {
                console.error('绘制波浪失败:', e);
                // 如果绘制失败,停止动画循环但不影响音乐播放
                if (animationId) {
                    cancelAnimationFrame(animationId);
                    animationId = null;
                }
            }
        }

        // 开始可视化
        function startVisualization() {
            try {
                if (!animationId) {
                    drawWave();
                }
            } catch (e) {
                console.error('启动可视化失败:', e);
                // 即使可视化失败,也要确保音乐能正常播放
            }
        }

        // 停止可视化
        function stopVisualization() {
            try {
                if (animationId) {
                    cancelAnimationFrame(animationId);
                    animationId = null;
                }

                // 清除画布
                if (canvasCtx) {
                    canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
                }
            } catch (e) {
                console.error('停止可视化失败:', e);
            }
        }

        // 初始化
        setVolume(80);
        initAudioVisualizer();

        // 页面加载完成后的欢迎信息
        window.addEventListener('load', () => {
            setTimeout(() => {
                showNotification('欢迎使用云音乐播放器!', 'success');
            }, 1000);
        });

        // 切换标签页
        function switchTab(tabName) {
            // 移除所有标签页的active类
            document.querySelectorAll('.tab-content').forEach(content => {
                content.classList.remove('active');
            });
            document.querySelectorAll('.tab-btn').forEach(btn => {
                btn.classList.remove('active');
            });

            // 激活选中的标签页
            document.getElementById(tabName + 'Tab').classList.add('active');
            // 激活当前点击的按钮
            event.currentTarget.classList.add('active');
        }

        // 解析网易云歌单
        async function parsePlaylist() {
            const playlistId = document.getElementById('playlistIdInput').value.trim();
            if (!playlistId) {
                showNotification('请输入歌单ID', 'warning');
                return;
            }

            const resultsContainer = document.getElementById('playlistResults');
            resultsContainer.innerHTML = `
                <div class="loading">
                    <i class="fas fa-spinner"></i>
                    <div>正在解析歌单...</div>
                </div>
            `;

            try {
                const response = await fetch(`${API_BASE}?types=playlist&id=${playlistId}&source=netease`);
                const data = await response.json();

                let songs = [];
                // 操,这个API返回的数据结构真他妈乱,得兼容好几种
                if (data && data.playlist && data.playlist.tracks) {
                    songs = data.playlist.tracks.map(track => ({
                        name: track.name,
                        artist: track.ar.map(a => a.name).join(' / '),
                        album: track.al.name,
                        id: track.id,
                        pic_id: track.al.pic_id_str || track.al.pic_str || track.al.pic,
                        lyric_id: track.id,
                        source: 'netease'
                    }));
                } else if (data && data.tracks) {
                     songs = data.tracks.map(track => ({
                        name: track.name,
                        artist: track.ar.map(a => a.name).join(' / '),
                        album: track.al.name,
                        id: track.id,
                        pic_id: track.al.pic_id_str || track.al.pic_str || track.al.pic,
                        lyric_id: track.id,
                        source: 'netease'
                    }));
                }

                if (songs.length > 0) {
                    playlistData = songs;
                    displaySearchResults(songs, 'playlistResults', playlistData);
                    showNotification(`成功加载 ${songs.length} 首歌曲`, 'success');
                } else {
                    resultsContainer.innerHTML = `
                        <div class="error">
                            <i class="fas fa-exclamation-triangle"></i>
                            <div>解析歌单失败,请检查ID是否正确或API是否正常</div>
                        </div>
                    `;
                }
            } catch (error) {
                console.error('解析歌单失败:', error);
                resultsContainer.innerHTML = `
                    <div class="error">
                        <i class="fas fa-wifi"></i>
                        <div>网络连接失败,请检查网络后重试</div>
                    </div>
                `;
            }
        }
    </script>
</body>
</html>

4. 补充

对于部署到服务器且开启了 https 的想要获取 酷我 源 http 开头的资源还需要一个代理接口,这里提供一个自用的 proxy.php 脚本:

<?php
// 关闭PHP的输出缓冲,以便于流式传输大文件
if (ob_get_level()) {
    ob_end_clean();
}

// 检查请求是API代理还是音频流代理
if (isset($_GET['stream_url'])) {
    // --- 音频流代理 (已修复) ---

    // 限制音频流的访问来源
    header("Access-Control-Allow-Origin: https://www.xxxxxx.com"); // 请替换成您自己的域名

    $audio_url = $_GET['stream_url'];

    // 白名单验证,只允许代理来自特定域名的音频
    $allowed_domains = [
        'music.126.net',      // 网易云音乐 (实际CDN域名)
        'music.163.com',      // 网易云音乐 (备用或API域名)
        'kuwo.cn',            // 酷我音乐 (使用 .kuwo.cn)
        'joox.com'
    ];

    $parsed_url = parse_url($audio_url);
    $host = $parsed_url['host'] ?? '';

    $is_allowed = false;
    foreach ($allowed_domains as $domain) {
        if (substr($host, -strlen($domain)) === $domain) {
            $is_allowed = true;
            break;
        }
    }

    if (!$is_allowed) {
        header("HTTP/1.1 403 Forbidden");
        echo "Access to this resource domain is not allowed.";
        exit;
    }

    // 再次验证URL格式
    if (!filter_var($audio_url, FILTER_VALIDATE_URL)) {
        header("HTTP/1.1 400 Bad Request");
        echo "Invalid URL";
        exit;
    }

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $audio_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HEADER, true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36');

    // --- [修复点 1] ---
    // 检查并转发浏览器发送的 Range 请求头,这是实现拖动进度条的关键
    if (isset($_SERVER['HTTP_RANGE'])) {
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Range: ' . $_SERVER['HTTP_RANGE']]);
    }

    $response = curl_exec($ch);

    if (curl_errno($ch)) {
        header("HTTP/1.1 502 Bad Gateway");
        echo "Failed to fetch audio stream.";
        curl_close($ch);
        exit;
    }

    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    $header_str = substr($response, 0, $header_size);
    $body = substr($response, $header_size);

    curl_close($ch);

    // --- [修复点 2] ---
    // 将源服务器返回的HTTP状态码(如 200 OK 或 206 Partial Content)转发给浏览器
    header("HTTP/1.1 " . $http_code);

    // 转发必要的头信息
    $headers = explode("\r\n", $header_str);
    foreach ($headers as $header) {
        // 转发 Content-Type, Content-Length, Accept-Ranges, Content-Range 这些对播放器至关重要的头信息
        if (preg_match('/^(Content-Type|Content-Length|Accept-Ranges|Content-Range)/i', $header)) {
            header($header);
        }
    }

    // 发送音频内容(如果请求了Range,这里就是部分内容)
    echo $body;

} else {
    // --- API元数据代理 (无需改动) ---

    $target_url = 'https://music-api.gdstudio.xyz/api.php';

    $query_params = $_GET;
    $query_string = http_build_query($query_params);
    $full_target_url = $target_url . '?' . $query_string;

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $full_target_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HEADER, true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36');
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);

    $response = curl_exec($ch);

    if (curl_errno($ch)) {
        header("HTTP/1.1 500 Internal Server Error");
        echo 'cURL Error: ' . curl_error($ch);
        exit;
    }

    $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    $header_str = substr($response, 0, $header_size);
    $body = substr($response, $header_size);

    curl_close($ch);

    // 只允许自己的域名访问
    header("Access-Control-Allow-Origin: https://www.xxxxxx.com"); // 请替换成您自己的域名

    $headers = explode("\r\n", $header_str);
    foreach ($headers as $header) {
        if (strpos(strtolower($header), 'content-type') !== false) {
            header($header);
            break; // 只转发 Content-Type 即可
        }
    }

    echo $body;
}

exit; // 确保脚本在这里终止
?>

使用方法:

// 替换成自己的路径
const API_BASE = 'https://www.xxxxxx.com/music/proxy.php';

将
audioPlayer.src = urlData.url;
替换成
audioPlayer.src = `${API_BASE}?stream_url=${encodeURIComponent(urlData.url)}`;
赞(0) 打赏
未经允许不得转载:文字咖 » 纯 HTML 开发一款带 API 接口的音乐播放器

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续提供更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫

微信扫一扫