出现的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)
播放器效果图
双击歌曲进行播放点击专辑封面可展示歌词
注:所有搜索结果皆来自互联网,在这里只是综合网络资源,这里不做任何商业用途,不储存任何音频文件,版权属于各音乐门户