Flask音乐平台应用的更新

出现的BUG说明(js代码挑位置的BUG)

发现将js代码以外联式的方式嵌入代码中(既以<script src="{{ url_for('static', filename='player.js') }}"></script>的方式引用)后会出现handlePlay未被定义的错误,开始以为是由于JavaScript函数加载顺序问题导致的,具体来说就是,页面在加载包含 handlePlay 函数的 player.js 之前就解析了音乐卡片的双击事件处理器。后来改变位置放到最前面问题依旧,后面取消外联式的方式直接在前端代码内部写入js代码(既将js代码写入<script></script>中)接着问题又出现了,直接写入后运行报Cannot read properties of null (reading 'addEventListener')的错误,可能又是加载顺序的问题。最后发现只要将js代码以放到body标签中最底部可以解决所用问题和BUG

  • 解决方案

将js代码直接在前端代码内部写入js代码(既将js代码写入<script></script>中)并且将js代码块放到body标签中(不要放到了body标签外)的最底部

更新内容

  • 新增换源功能,目前有两个源可以切换
  • 优化下载逻辑
  • 新增自定义播放器,放弃原有浏览器默认的播放器,自定义UI界面
  • 新增歌曲封面,歌词显示

项目结构

      musicPlay/
               ├── app.py               # Flask主应用
               ├── static/              # 静态资源目录
               │        ├── css/            # 样式表
               │        └── js/             # JavaScript文件
               └── templates/          # HTML模板
                          └── index.html      # 主界面模板

安装依赖

pip install flask
pip install flask-session
pip install requests
pip install lxml

前端代码

index.html

<!DOCTYPE html>
<html>

<head>
    <!-- 页面元信息设置 -->
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
    <!-- 页面标题 -->
    <title>音乐下载器</title>
    <!-- 空图标占位符 -->
    <link rel="icon" href="data:;base64,=">
    <!-- 页面全局样式定义 -->
    <link rel="stylesheet" href="{{ url_for('static', filename='player.css') }}">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
</head>

<body>
    <!-- 页面主标题 -->
    <h1>音乐搜索与下载</h1>

    <!-- 状态消息显示区域 -->
    <div class="status-messages">
        {% with messages = get_flashed_messages() %}
        {% if messages %}
        <div class="alert">
            {{ messages[0] }}
            <button onclick="this.parentElement.style.display='none'">×</button>
        </div>
        {% endif %}
        {% endwith %}
    </div>

    <!-- 搜索表单 -->
    <form action="/search" method="post" style="display: flex; gap: 10px; max-width: 600px; margin: 20px 0;">
        <input type="text" id="keyword" name="keyword" required placeholder="输入歌曲名或歌手...">

        <button id="search-btn" type="submit">
            <!-- 搜索图标 -->
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"
                style="margin-top: 2px;">
                <path
                    d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z" />
            </svg>
            搜索
        </button>
        <!-- 音乐源选择下拉菜单 -->
        <select name="source" class="form-control" onchange="showSourceNotice(this.value)"
            style="margin-left: 10px; border: 0px;">
            <option value="qeecc">音乐源1</option>
            <option value="4c44">音乐源2</option>
        </select>
    </form>

    <!-- 搜索加载提示 -->
    <div id="search-loading" style="display:none; margin:15px 0; color:#666;">
        ⌛ 正在搜索中,请稍候...
        <div class="loader" style="display:inline-block; margin-left:8px;"></div>
    </div>

    <!-- 搜索结果区域 -->
    {% if results %}
    <div style="display: flex; justify-content: space-between; align-items: center; margin: 20px 0;">
        <h2>搜索结果</h2>
        <button id="global-download-btn" onclick="handleGlobalDownload()">
            ⬇️ 下载当前播放歌曲
        </button>
    </div>
    <div class="music-grid">
        <!-- 循环渲染每首歌曲 -->
        {% for idx, result in results %}
        <div class="music-card" ondblclick="handlePlay({{ loop.index }});">
            <div style="margin-top: 10px;">
                <div class="song-title">
                    <!-- 处理歌曲标题格式 -->
                    {% set name_parts = result[0].split('《') %}
                    {% if name_parts|length > 1 %}
                    {{ name_parts[1].split('》')[0] if '》' in name_parts[1] else name_parts[1] }}
                    {% else %}
                    {{ result[0] }}
                    {% endif %}
                </div>
                <div class="song-info">
                    <!-- 提取歌手信息 -->
                    {% set name_parts = result[0].split('《') %}
                    {{ name_parts[0] if name_parts|length > 0 else '' }}
                </div>
            </div>
            <!-- 隐藏的歌曲索引 -->
            <input type="hidden" class="song-index" value="{{ loop.index }}">
        </div>
        {% endfor %}
    </div>
    {% endif %}
    <div class="lyrics-container">
        <div class="lyrics">
            <p>测试</p>
            <p>测试</p>
            <p>测试</p>
            <p>测试</p>
            <p>测试</p>
            <p>测试</p>
            <p>测试</p>
        </div>
    </div>
    <!-- 音乐播放器 -->
    <div class="music-player">
        <!-- 控制按钮组 -->
        <div class="control-buttons">
            <button class="control-btn prev-btn">⏮</button>
            <button class="control-btn play-btn">▶</button>
            <button class="control-btn next-btn">⏭</button>
        </div>

        <!-- 专辑封面区域 -->
        <div class="album-cover">
            <img id="album-cover-img" src="" alt="专辑封面"
                style="width: 100%; height: 100%; border-radius: 10px; display: none;">
            <div id="cover-placeholder">图片加载中</div>
        </div>

        <!-- 歌曲信息显示区域 -->
        <div class="song-info">
            <span class="song-title-ui" id="player-song-title">未播放歌曲</span>
            <div class="progress-container">
                <input type="range" class="progress-bar" min="0" max="100" value="0">
            </div>
            <div class="time-info">
                <span class="current-time">0:00</span>
                <span class="total-time">0:00</span>
            </div>
        </div>

        <!-- 播放控制区域 -->
        <div class="player-controls">
            <span class="playback-rate">1.0x</span>
            <div class="volume-control">
                <span class="volume-icon">🔊</span>
                <input type="range" class="volume-slider" min="0" max="100" value="80">
            </div>
            <button class="download-btn" id="player-download-btn">
                <i class="fa fa-download"></i>
            </button>
        </div>

        <!-- 加载状态提示 -->
        <div id="loading-status">
            ⌛ 加载中...
        </div>
        <!-- 隐藏的音频播放器 -->
        <audio id="main-player" style="display: none;"></audio>
    </div>

    <!-- 下载状态显示区域 -->
    <div id="download-status">
        {% if message %}
        <div class="alert">
            {{ message }}
            <button onclick="this.parentElement.style.display='none'">×</button>
        </div>
        {% endif %}
    </div>

    <!-- 页面主脚本 -->
    <script src="{{ url_for('static', filename='player.js') }}">

    </script>
</body>

</html>

 player.css

/* 专辑封面加载占位符样式 */
#cover-placeholder {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: #666;
}

/* 专辑封面图片过渡效果 */
#album-cover-img {
    transition: opacity 0.3s ease;
}

/* 播放器整体容器样式 */
.music-player {
    display: flex;
    align-items: center;
    width: 95%;
    height: 90px;
    border-radius: 15px 15px 0 0;
    padding: 0 20px;
    background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
    box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3);
    color: white;
    position: fixed;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 1000;
}

/* 播放器顶部装饰条 */
.music-player::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 5px;
    background: linear-gradient(90deg, #3498db, #9b59b6, #e74c3c);
}

/* 控制按钮容器 */
.control-buttons {
    display: flex;
    align-items: center;
    gap: 15px;
    margin-right: 25px;
}

/* 通用控制按钮样式 */
.control-btn {
    width: 45px;
    height: 45px;
    border-radius: 50%;
    border: none;
    background-color: rgba(255, 255, 255, 0.2);
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    font-size: 20px;
    transition: all 0.3s ease;
}

/* 控制按钮悬停效果 */
.control-btn:hover {
    background-color: rgba(255, 255, 255, 0.3);
    transform: scale(1.05);
}

/* 播放按钮特殊样式 */
.play-btn {
    width: 60px;
    height: 60px;
    background-color: white;
    color: #2c3e50;
    font-size: 24px;
}

/* 专辑封面容器样式 */
.album-cover {
    position: relative;
    overflow: hidden;
    width: 80px;
    height: 80px;
    border-radius: 10px;
    background-color: #ddd;
    margin-right: 25px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    color: #666;
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
    transition: all 0.3s ease;
}

.album-cover:hover {
    transform: scale(1.05) rotate(3deg);
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
    filter: brightness(1.1);
}

.album-cover:hover::after {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 24px;
    color: rgba(255, 255, 255, 0.9);
    text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
    animation: pulse 1.5s infinite;
}

@keyframes pulse {
    0% {
        opacity: 0.8;
        transform: translate(-50%, -50%) scale(1);
    }

    50% {
        opacity: 1;
        transform: translate(-50%, -50%) scale(1.2);
    }

    100% {
        opacity: 0.8;
        transform: translate(-50%, -50%) scale(1);
    }
}

/* 歌曲信息区域样式 */
.song-info {
    flex: 1;
    margin-right: 25px;
}

/* 歌曲标题UI样式 */
.song-title-ui {
    font-size: 18px;
    font-weight: 500;
    margin-bottom: 8px;
    display: block;
    color: #ecf0f1;
}

/* 进度条容器 */
.progress-container {
    width: 100%;
    margin-bottom: 8px;
}

/* 进度条样式 */
.progress-bar {
    width: 100%;
    height: 6px;
    background-color: rgba(255, 255, 255, 0.2);
    border-radius: 3px;
    appearance: none;
    -webkit-appearance: none;
    cursor: pointer;
}

/* 进度条滑块样式 */
.progress-bar::-webkit-slider-thumb {
    appearance: none;
    -webkit-appearance: none;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: #3498db;
    cursor: pointer;
}

/* 时间信息显示样式 */
.time-info {
    display: flex;
    justify-content: space-between;
    font-size: 14px;
    color: rgba(255, 255, 255, 0.7);
}

/* 播放控制区域样式 */
.player-controls {
    display: flex;
    align-items: center;
    gap: 20px;
}

/* 播放速率控制样式 */
.playback-rate {
    background-color: rgba(255, 255, 255, 0.2);
    padding: 5px 10px;
    border-radius: 12px;
    font-size: 14px;
    cursor: pointer;
}

/* 音量控制容器 */
.volume-control {
    display: flex;
    align-items: center;
    gap: 8px;
}

/* 音量图标样式 */
.volume-icon {
    font-size: 18px;
    color: rgba(255, 255, 255, 0.8);
}

/* 音量滑块样式 */
.volume-slider {
    width: 90px;
    height: 6px;
    background-color: rgba(255, 255, 255, 0.2);
    border-radius: 3px;
    appearance: none;
    -webkit-appearance: none;
}

/* 音量滑块按钮样式 */
.volume-slider::-webkit-slider-thumb {
    appearance: none;
    -webkit-appearance: none;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.8);
}

/* 下载按钮样式 */
.download-btn {
    width: 36px;
    height: 36px;
    border-radius: 8px;
    border: none;
    background-color: rgba(52, 152, 219, 0.8);
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    font-size: 16px;
    gap: 8px;
}

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

/* 加载动画样式 */
.loader {
    width: 12px;
    height: 12px;
    border: 2px solid #4CAF50;
    border-radius: 50%;
    border-top-color: transparent;
    animation: spin 1s linear infinite;
}

/* 加载动画关键帧 */
@keyframes spin {
    0% {
        transform: rotate(0deg);
    }

    100% {
        transform: rotate(360deg);
    }
}

/* 音乐卡片样式 */
.music-card {
    border: 1px solid #e0e0e0;
    border-radius: 12px;
    padding: 15px;
    transition: all 0.3s ease;
    cursor: pointer;
}

/* 歌曲标题样式 */
.song-title {
    font-size: 16px;
    color: #333;
}

/* 歌曲信息文本样式 */
.song-info {
    font-size: 12px;
    color: #999;
    margin-top: 8px;
}

/* 音乐卡片悬停效果 */
.music-card:hover {
    background-color: #f8f8f8;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    transform: translateY(-2px);
    border-color: #4CAF50;
}

/* 按钮悬停效果 */
button:hover {
    background: #45a049 !important;
}

/* 输入框聚焦效果 */
input:focus {
    border-color: #45a049;
    box-shadow: 0 0 8px rgba(76, 175, 80, 0.3);
}

/* 网格布局容器 */
.music-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 20px;
    padding: 20px 0;
    width: 100%;
    box-sizing: border-box;
}

/* 全局播放器位置 */
#global-player {
    position: fixed;
    bottom: 20px;
    right: 20px;
    left: 20px;
}

/* 加载状态提示样式 */
#loading-status {
    display: none;
    margin-bottom: 10px;
    color: #666;
}

/* 主播放器宽度 */
#main-player {
    width: 100%;
}

/* 下载状态区域 */
#download-status {
    margin-top: 20px;
}

/* 全局下载按钮样式 */
#global-download-btn {
    background: #4CAF50;
    color: white;
    border: none;
    padding: 8px 20px;
    border-radius: 20px;
    cursor: pointer;
}

/* 搜索按钮样式 */
#search-btn {
    background: #4CAF50;
    color: white;
    border: none;
    padding: 12px 30px;
    border-radius: 25px;
    cursor: pointer;
    font-size: 16px;
    transition: background 0.3s ease;
    display: flex;
    align-items: center;
    gap: 8px;
}

/* 搜索输入框样式 */
#keyword {
    flex: 1;
    padding: 12px;
    border: 2px solid #4CAF50;
    border-radius: 25px;
    font-size: 16px;
    outline: none;
    transition: border-color 0.3s ease;
}

.download-btn:hover {
    background-color: #3498db;
    transform: translateY(-2px);
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}

/* 新增歌词样式 */
.lyrics-container {
    position: fixed;
    bottom: 110px;
    /* 位于播放器上方 */
    left: 50%;
    max-height: 0;
    padding: 0 20px;
    transform: translateX(-50%);
    width: 95%;
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.5s ease, padding 0.5s ease;
    background: rgba(44, 62, 80, 0.95);
    border-radius: 15px 15px 0 0;
    padding: 0 20px;
    box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.2);
    z-index: 10;
}

.lyrics-container.expanded {
    max-height: 85%;
    padding: 10px;
}

.lyrics {
    color: #ecf0f1;
    text-align: center;
    line-height: 1.8;
    font-size: 16px;
    max-height: 250px;
    overflow-y: auto;
    -ms-overflow-style: none;
    scrollbar-width: none;
    padding: 10px;
    scroll-behavior: smooth;
}

.lyrics::-webkit-scrollbar {
    display: none;
    /* Chrome, Safari and Opera */
}

.lyrics p {
    margin: 8px 0;
    transition: all 0.3s;
}

.lyrics p.highlight {
    color: #3498db;
    font-size: 20px;
    font-weight: bold;
    transform: scale(1.05);
}

player.js


/**
 * 处理歌曲播放功能
 * @param {number} index - 要播放的歌曲索引
 */
// 在handlePlay函数中添加歌词请求
async function handlePlay(index) {
    currentSongIndex = index;
    const statusDiv = document.getElementById('loading-status');
    statusDiv.style.display = 'block';

    try {
        // 获取音频URL和封面信息
        const response = await fetch(`/get_audio_url/${index}`);
        const data = await response.json();

        const coverImg = document.getElementById('album-cover-img');
        const placeholder = document.getElementById('cover-placeholder');
        const lyricsResponse = await fetch(`/lyrics/${index}`);
        if (!lyricsResponse.ok) throw new Error('歌词请求失败');
        const lyricsData = await lyricsResponse.json();
        // 添加数据校验
        if (lyricsData && lyricsData.lyrics) {
            updateLyrics(lyricsData.lyrics);
        } else {
            updateLyrics('');
        }
        // 封面加载成功处理
        coverImg.onload = () => {
            placeholder.style.display = 'none';
            coverImg.style.display = 'block';
        };
        // 封面加载失败处理
        coverImg.onerror = () => {
            placeholder.textContent = '封面加载失败';
            coverImg.style.display = 'none';
        };

        // 设置封面图片源
        coverImg.src = data.pic;
        // 更新播放器音频源
        audioPlayer.src = data.url;
        audioPlayer.play();
        isPlaying = true;
        playBtn.textContent = '⏸';

        // 更新播放器显示的歌曲标题
        const songElement = document.querySelector(`.music-card:nth-child(${index}) .song-title`);
        songTitleDisplay.textContent = songElement.textContent;

    } catch (error) {
        console.error('加载失败:', error);
        showStatusMessage('播放失败: ' + error.message, 'error');
    } finally {
        statusDiv.style.display = 'none';
    }
}
// 获取播放器相关DOM元素
const lyricsContainer = document.querySelector('.lyrics-container');
const albumCover = document.querySelector('.album-cover');
const audioPlayer = document.getElementById('main-player');
const playBtn = document.querySelector('.play-btn');
const progressBar = document.querySelector('.progress-bar');
const currentTimeDisplay = document.querySelector('.current-time');
const totalTimeDisplay = document.querySelector('.total-time');
const songTitleDisplay = document.getElementById('player-song-title');
const volumeSlider = document.querySelector('.volume-slider');
const volumeIcon = document.querySelector('.volume-icon');
const playbackRate = document.querySelector('.playback-rate');
const downloadBtn = document.getElementById('player-download-btn');
const prevBtn = document.querySelector('.prev-btn');
const nextBtn = document.querySelector('.next-btn');

// 播放/暂停功能实现
let isPlaying = false;
playBtn.addEventListener('click', () => {
    if (audioPlayer.src) {
        isPlaying = !isPlaying;
        if (isPlaying) {
            audioPlayer.play();
            playBtn.textContent = '⏸';
        } else {
            audioPlayer.pause();
            playBtn.textContent = '▶';
        }
    }
});

// 更新播放进度
audioPlayer.addEventListener('timeupdate', () => {
    // 计算播放进度百分比
    const percentage = (audioPlayer.currentTime / audioPlayer.duration) * 100;
    progressBar.value = percentage || 0;

    // 格式化并显示当前播放时间
    const currentMinutes = Math.floor(audioPlayer.currentTime / 60);
    const currentSeconds = Math.floor(audioPlayer.currentTime % 60);
    currentTimeDisplay.textContent = `${currentMinutes}:${currentSeconds < 10 ? '0' + currentSeconds : currentSeconds}`;
});



// 音频元数据加载完成时更新总时长
audioPlayer.addEventListener('loadedmetadata', () => {
    // 格式化并显示总时长
    const totalMinutes = Math.floor(audioPlayer.duration / 60);
    const totalSeconds = Math.floor(audioPlayer.duration % 60);
    totalTimeDisplay.textContent = `${totalMinutes}:${totalSeconds < 10 ? '0' + totalSeconds : totalSeconds}`;
});

// 进度条拖动控制
progressBar.addEventListener('input', () => {
    // 根据进度条值计算跳转位置
    const seekTime = (progressBar.value / 100) * audioPlayer.duration;
    audioPlayer.currentTime = seekTime;
});

// 音量控制
volumeSlider.addEventListener('input', () => {
    // 设置播放器音量
    audioPlayer.volume = volumeSlider.value / 100;
    // 根据音量值更新图标
    if (volumeSlider.value == 0) {
        volumeIcon.textContent = '🔇';
    } else if (volumeSlider.value < 50) {
        volumeIcon.textContent = '🔈';
    } else {
        volumeIcon.textContent = '🔊';
    }
});

// 播放速率控制
playbackRate.addEventListener('click', () => {
    // 可用播放速率选项
    const rates = [0.75, 1.0, 1.25, 1.5, 2.0];
    const currentRate = audioPlayer.playbackRate;
    const currentIndex = rates.indexOf(currentRate);
    // 循环切换速率
    const nextIndex = (currentIndex + 1) % rates.length;
    audioPlayer.playbackRate = rates[nextIndex];
    playbackRate.textContent = `${rates[nextIndex]}x`;
});

/**
 * 上一曲按钮事件处理
 */
prevBtn.addEventListener('click', () => {
    if (currentSongIndex > 1) {
        handlePlay(currentSongIndex - 1);
    }
});

/**
 * 下一曲按钮事件处理
 */
nextBtn.addEventListener('click', () => {
    const totalSongs = document.querySelectorAll('.music-card').length;
    if (currentSongIndex < totalSongs) {
        handlePlay(currentSongIndex + 1);
    }
});

// 表单提交拦截处理
const form = document.querySelector('form');
form.addEventListener('submit', function (e) {
    // 显示搜索加载状态
    document.getElementById('search-loading').style.display = 'block';
    document.getElementById('search-btn').disabled = true;
});

// 搜索结果渲染后隐藏加载提示
{% if results %}
document.getElementById('search-loading').style.display = 'none';
document.getElementById('search-btn').disabled = false;
{% endif %}

// 当前播放歌曲索引
let currentSongIndex = null;

/**
 * 显示状态消息
 * @param {string} message - 要显示的消息内容
 * @param {string} type - 消息类型(success/error/warning)
 */
function showStatusMessage(message, type) {
    const statusDiv = document.getElementById('download-status');
    statusDiv.innerHTML = `
        <div class="alert ${type}" style="margin-top:10px;">
            ${message}
            <p style="font-size:12px;margin:5px 0 0 0;color:#666;">
                默认下载路径:<mcfolder name="Downloads" path="C:/Users/zhang/Downloads"></mcfolder>
                (可通过浏览器设置修改)
            </p>
            <button onclick="this.parentElement.style.display='none'">×</button>
        </div>
    `;
}

/**
 * 处理全局下载功能
 */
async function handleGlobalDownload() {
    // 检查是否有当前播放的歌曲
    if (!currentSongIndex) {
        showStatusMessage('请先双击选择要下载的歌曲', 'warning');
        return;
    }
    try {
        // 获取当前歌曲索引
        const index = document.querySelector(`.music-card:nth-child(${currentSongIndex}) .song-index`).value;
        // 创建隐藏表单触发下载
        const form = document.createElement('form');
        form.method = 'POST';
        form.action = '/download';
        form.style.display = 'none';

        const input = document.createElement('input');
        input.type = 'hidden';
        input.name = 'song_index';
        input.value = index;
        form.appendChild(input);

        document.body.appendChild(form);
        form.submit();
        document.body.removeChild(form);
    } catch (error) {
        showStatusMessage('下载失败: ' + error.message, 'error');
    }
}

// 绑定下载按钮事件
downloadBtn.addEventListener('click', handleGlobalDownload);
albumCover.addEventListener('click', () => {
    lyricsContainer.classList.toggle('expanded');
});
function updateLyrics(lrcText) {
    const lyricsContainer = document.querySelector('.lyrics');

    const filteredLyrics = lrcText
        .replace(/歌词由\s*www\s*\.\s*45hk\s*\.\s*com\s*提供/gi, '')
        .split('\n')
        .filter(line => line.trim());

    // 添加空值处理
    if (!lrcText || typeof lrcText !== 'string') {
        lyricsContainer.innerHTML = '<p>暂无歌词</p>';
        return;
    }

    const lines = lrcText.split(/\n/).filter(line => line.trim() !== '');
    const lyricsDiv = document.querySelector('.lyrics');
    lyricsDiv.innerHTML = '';
    window.lyricMap = [];

    lines.forEach(line => {
        // 匹配多个时间标签和歌词内容
        const timeTags = line.match(/\[(\d{2}:\d{2}(?:\.\d+)?)\]/g);
        const text = line.replace(/\[.*?\]/g, '').trim();

        if (timeTags && text) {
            timeTags.forEach(tag => {
                const timeStr = tag.match(/\d+:\d{2}(?:\.\d{2})?/)[0];
                const [minutes, seconds] = timeStr.split(/[:.]/);
                const time = parseFloat(minutes) * 60 +
                    parseFloat(seconds) +
                    (timeStr.includes('.') ? parseFloat(line.match(/\.(\d{2})/)[1]) / 100 : 0);

                const p = document.createElement('p');
                p.textContent = text;
                p.dataset.time = time.toFixed(2);
                lyricsDiv.appendChild(p);
                window.lyricMap.push({
                    time: parseFloat(time.toFixed(2)),
                    element: p
                });
            });
        } else if (text) { // 处理无时间标签的歌词
            const p = document.createElement('p');
            p.textContent = text;
            p.classList.add('static-lyric');
            lyricsDiv.appendChild(p);
        }
    });
    window.lyricMap.sort((a, b) => a.time - b.time);
    // 添加音频时间更新监听
    audioPlayer.addEventListener('timeupdate', () => {
        const currentTime = audioPlayer.currentTime;
        // 查找当前对应的歌词行
        let activeIndex = window.lyricMap.findIndex(item => item.time > currentTime) - 1;
        if (activeIndex < 0) activeIndex = window.lyricMap.length - 1;

        // 更新高亮样式
        window.lyricMap.forEach((item, index) => {
            item.element.classList.toggle('highlight', index === activeIndex);
            if (index === activeIndex) {
                item.element.scrollIntoView({
                    behavior: 'smooth',
                    block: 'center'
                });
            }
        });
    });
}

后端代码

app.py

# 由于是从上一个项目上修改来的就有一些冗余无用的代码或错误(屎山代码作者太菜,越改越错)
# 安装依赖
# pip install flask
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify, Response
# IDE提示错误不用管
from flask_session import Session
import requests, urllib.parse
from requests.exceptions import RequestException
from lxml import etree
from random import choice
import time

# 多源配置
SOURCE_CONFIG = {
    'qeecc': {
        'base_url': 'https://www.qeecc.com/so/{keyword}.html',
        'page_url': 'https://www.qeecc.com/so/{keyword}/{page}.html',
        'xpaths': {
            'results': '//div[@class="play_list"]/ul/li/div[@class="name"]/a'
        },
        'api_endpoint': 'http://www.qeecc.com/js/play.php',  # 添加API端点
        'referer_pattern': 'https://www.qeecc.com/song/{music_id}.html'
    },
    '4c44': {
        'base_url': 'http://www.4c44.com/so/{keyword}.html',
        'page_url': 'http://www.4c44.com/so/{keyword}/{page}.html',
        'xpaths': {
            'results': '//div[@class="play_list"]/ul/li/div[@class="name"]/a'
        },
        'api_endpoint': 'http://www.4c44.com/js/play.php',  # 添加镜像源API端点
        'referer_pattern': 'http://www.4c44.com/song/{music_id}.html'
    }
}
app = Flask(__name__)
app.secret_key = 'your_secret_key_here'
user_agents = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15',
    'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko',
    'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.110 Mobile Safari/537.36',
]

# 使用 time 模块获取当前时间的时间戳(秒数),只生成一次的时间戳,懒得改了
now_time = int(time.time())


def get_user_agent(user_agents):
    """
    从给定的用户代理列表中随机选择一个User-Agent

    参数:
        user_agents (list): 包含多个User-Agent字符串的列表

    返回:
        dict: 包含随机选择的User-Agent的字典,格式为{'User-Agent': '...'}
    """
    return {'User-Agent': choice(user_agents)}


def search_music(keyword, headers, source='qeecc'):
    """
    在指定音乐源上搜索关键词并返回结果列表

    参数:
        keyword (str): 搜索关键词
        headers (dict): HTTP请求头
        source (str): 音乐源标识,默认为'qeecc'

    返回:
        list: 包含元组(歌曲名, 详情页URL)的结果列表
    """
    config = SOURCE_CONFIG[source]
    all_results = []
    global_index = 1

    # 遍历1-10页搜索结果
    for page in range(1, 11):
        # 处理不同源的URL参数差异
        page_url = config['page_url'].format(
            keyword=keyword,
            page=page
        )

        try:
            # 发送搜索请求
            response = requests.get(page_url, headers=headers, timeout=20)
            response.raise_for_status()
            print(f'响应状态码:{response.status_code}')

            # 镜像源404处理逻辑
            if source == '4c44' and response.status_code == 404:
                print('镜像源404,停止采集')
                break

            # 解析HTML响应
            root = etree.HTML(response.text)
            items = root.xpath(config['xpaths']['results'])

            # 提取歌曲名称和URL
            sing_names = [item.xpath('./text()')[0] for item in items if item.xpath('./text()')]
            music_urls = [item.xpath('./@href')[0] for item in items if item.xpath('./@href')]

            # 无结果时提前终止搜索
            if not sing_names:
                print(f'第{page}页无结果,停止采集')
                break

            # 合并当前页结果到总列表
            page_results = list(zip(sing_names, music_urls))
            for idx, (name, url) in enumerate(page_results, start=global_index):
                all_results.append((name, url))
                print(f'{idx}. {name}')

            global_index += len(page_results)
            time.sleep(1)

        except RequestException as e:
            print(f"请求失败: {e}")
            continue  # 继续下一页而不是直接返回

    return all_results


@app.route('/')
def index():
    """
    渲染主页面

    返回:
        HTML: 渲染后的index.html模板
    """
    return render_template('index.html')


@app.route('/search', methods=['POST'])
def search():
    """
    处理搜索请求并返回结果

    返回:
        HTML: 渲染后的index.html模板,包含搜索结果或错误信息
    """
    try:
        # 获取音乐源和搜索关键词
        source = request.form.get('source', 'qeecc')
        session['current_source'] = source  # 保存当前源到会话
        raw_keyword = request.form['keyword']

        # 源特定配置
        if source == 'qeecc':
            keyword = urllib.parse.quote(raw_keyword)
            headers = {
                'Referer': 'https://www.qeecc.com/',
                'Origin': 'https://www.qeecc.com',
                'X-Requested-With': 'XMLHttpRequest',
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0',
                'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
            }
        else:
            keyword = raw_keyword.replace(' ', '+')
            headers = {
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
                'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
                'Cache-Control': 'max-age=0',
                'Connection': 'keep-alive',
                'DNT': '1',
                'Referer': 'http://www.4c44.com/',
                'Upgrade-Insecure-Requests': '1',
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0',
            }

        # 执行搜索并保存结果
        results = search_music(keyword, headers, source=source)
        session['search_results'] = results
        return render_template('index.html', results=enumerate(results, start=1))

    except Exception as e:
        app.logger.error(f'搜索失败: {str(e)}')
        return render_template('index.html', error=f"搜索失败: {str(e)}"), 500


@app.route('/download', methods=['POST'])
def download():
    """
    处理音乐下载请求

    返回:
        Response: 音频文件流或JSON错误响应
    """
    try:
        # 获取歌曲索引
        song_index = int(request.form.get('song_index'))
        if 'search_results' not in session:
            return jsonify(success=False, error="请先进行搜索")

        results = session['search_results']
        sing_name, music_url = results[song_index - 1]
        music_id = music_url.split("/")[-1].split(".")[0]

        # 获取当前源配置
        if 'current_source' not in session:
            return jsonify(success=False, error="请先进行搜索")
        source = session['current_source']
        config = SOURCE_CONFIG[source]

        # 获取音乐文件URL
        res = requests.post(
            config['api_endpoint'],
            cookies={
                '546114864cb24c0e5075a8cb0f0e3947': 'f4b987147576c0532595d00c5ee3fb22',
                'Hm_tf_6rdiytabw7z': '1744553202',
                'Hm_lvt_6rdiytabw7z': '1744553202,1744553258',
                'Hm_lvt_00c97102af6d427421274b7ae48b4c2c': '1744553215,1744553258',
                'Hm_lpvt_00c97102af6d427421274b7ae48b4c2c': '1744553258',
                'HMACCOUNT': 'FE7CDDB0B601AF3C',
                'Hm_lpvt_6rdiytabw7z': f'{now_time}',
            },
            headers={
                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                'User-Agent': choice(user_agents),
                'Referer': config['referer_pattern'].format(music_id=music_id),
                'Origin': config['api_endpoint'].split('/')[2]
            },
            data={'id': music_id, 'type': 'music'},
            timeout=10
        )
        res.raise_for_status()
        sing_data = res.json()
        sing_url = sing_data['url'].replace('http://', 'https://')

        # 获取音频文件流
        audio_response = requests.get(sing_url, stream=True)
        audio_response.raise_for_status()

        # 验证音频文件内容
        content_length = audio_response.headers.get('Content-Length')
        if not content_length or int(content_length) == 0:
            raise RequestException("获取到空的音频文件")

        # 设置下载响应头
        response_headers = {
            'Content-Type': audio_response.headers.get('Content-Type', 'audio/mpeg'),
            'Content-Disposition': f'attachment; filename="{urllib.parse.quote(sing_name)}.mp3"',
            'Content-Length': content_length
        }
        # 返回文件流响应
        return Response(audio_response.iter_content(chunk_size=1024), headers=response_headers)
    except Exception as e:
        app.logger.error(f'下载失败: {str(e)}')
        return jsonify(success=False, error=f"下载失败: {str(e)}")


# 使用服务器端会话存储方案
app.config['SESSION_TYPE'] = 'filesystem'
app.config['SESSION_FILE_DIR'] = './flask_session'
Session(app)


@app.route('/get_audio_url/<int:index>')
def get_audio_url(index):
    """
    获取指定索引歌曲的音频URL

    参数:
        index (int): 歌曲在搜索结果中的索引

    返回:
        JSON: 包含音频URL和封面的响应或错误信息
    """
    if 'search_results' not in session or 'current_source' not in session:
        return jsonify(error="请先搜索"), 403

    source = session['current_source']
    config = SOURCE_CONFIG[source]
    results = session['search_results']
    # 验证索引有效性
    if index < 1 or index > len(results):
        return jsonify(error="无效的歌曲索引"), 404

    sing_name, music_url = results[index - 1]
    music_id = music_url.split("/")[-1].split(".")[0]

    try:
        # 请求音频URL
        res = requests.post(
            config['api_endpoint'],
            cookies={
                '546114864cb24c0e5075a8cb0f0e3947': 'f4b987147576c0532595d00c5ee3fb22',
                'Hm_tf_6rdiytabw7z': '1744553202',
                'Hm_lvt_6rdiytabw7z': '1744553202,1744553258',
                'Hm_lvt_00c97102af6d427421274b7ae48b4c2c': '1744553215,1744553258',
                'Hm_lpvt_00c97102af6d427421274b7ae48b4c2c': '1744553258',
                'HMACCOUNT': 'FE7CDDB0B601AF3C',
                'Hm_lpvt_6rdiytabw7z': f'{now_time}',
            },
            headers={
                'User-Agent': choice(user_agents),
                'Referer': config['referer_pattern'].format(music_id=music_id),
                'Origin': config['api_endpoint'].split('/')[2]
            },
            data={'id': music_id, 'type': 'music'},
            timeout=10
        )
        res.raise_for_status()
        
        # 返回音频URL和封面
        return jsonify(
            url=res.json()['url'].replace('http://', 'https://'),
            pic=res.json().get('pic', ''),
            lkid=res.json().get('lkid', '')  # 新增lkid参数
        ), 200, {
            'Access-Control-Allow-Origin': '*',
            'Cache-Control': 'no-cache'
        }
    except Exception as e:
        app.logger.error(f'音频获取失败: {str(e)}')
        return jsonify(error="无法获取音频"), 500


@app.route('/lyrics/<int:index>')
def get_lyrics(index):
    try:
        # 直接调用get_audio_url的逻辑而不是请求接口
        if 'search_results' not in session or 'current_source' not in session:
            return jsonify(error="请先搜索"), 403
            
        source = session['current_source']
        config = SOURCE_CONFIG[source]
        results = session['search_results']
        
        if index < 1 or index > len(results):
            return jsonify(error="无效的歌曲索引"), 404
            
        _, music_url = results[index - 1]
        music_id = music_url.split("/")[-1].split(".")[0]
        
        # 直接复用get_audio_url的请求逻辑获取lkid
        res = requests.post(
            config['api_endpoint'],
             cookies={
                '546114864cb24c0e5075a8cb0f0e3947': 'f4b987147576c0532595d00c5ee3fb22',
                'Hm_tf_6rdiytabw7z': '1744553202',
                'Hm_lvt_6rdiytabw7z': '1744553202,1744553258',
                'Hm_lvt_00c97102af6d427421274b7ae48b4c2c': '1744553215,1744553258',
                'Hm_lpvt_00c97102af6d427421274b7ae48b4c2c': '1744553258',
                'HMACCOUNT': 'FE7CDDB0B601AF3C',
                'Hm_lpvt_6rdiytabw7z': f'{now_time}',
            },
            headers={
                'User-Agent': choice(user_agents),
                'Referer': config['referer_pattern'].format(music_id=music_id),
                'Origin': config['api_endpoint'].split('/')[2]
            },
            data={'id': music_id, 'type': 'music'},
            timeout=10
        )
        res.raise_for_status()
        lkid = res.json().get('lkid', '')

        # 请求歌词
        headers = {
            'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0'
        }
        params = {'cid': f'{lkid}'}
        response = requests.get('https://js.eev3.com/lrc.php', params=params, headers=headers)
        return jsonify(lyrics=response.json()['lrc'])
    except Exception as e:
        return jsonify(error=str(e)), 500
        

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

播放器效果图

 双击歌曲进行播放点击专辑封面可展示歌词

注:所有搜索结果皆来自互联网,在这里只是综合网络资源,这里不做任何商业用途,不储存任何音频文件,版权属于各音乐门户

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值