在这篇博文中,我们将介绍如何使用Flask框架构建一个简易的音乐搜索与下载平台。这个平台允许用户输入歌曲名或歌手名,搜索相关音乐,并下载搜索结果中的音乐文件。整个项目将涉及前端页面设计、后端逻辑处理以及音乐文件的获取。
环境准备
在开始之前,请确保你已经安装了以下环境:
- Python 3.x
- Flask
- requests库
- lxml库
你可以通过以下命令安装所需的Python库:
pip install flask requests lxml
项目结构
项目的主要文件结构如下:
/music_downloader
├── app.py
├── templates
│ └── index.html
├── flask_session
Flask应用设置
首先,我们需要创建一个Flask应用实例,并配置一些基本设置。
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify
from flask_session import Session
import requests, urllib.parse
from requests.exceptions import RequestException
from lxml import etree
from random import choice
import os
import time
import re
app = Flask(__name__)
app.secret_key = 'your_secret_key_here'
# 使用文件系统存储会话
app.config['SESSION_TYPE'] = 'filesystem'
app.config['SESSION_FILE_DIR'] = './flask_session'
Session(app)
音乐搜索功能
接下来,我们实现音乐搜索功能。这个函数会发送HTTP请求到音乐搜索网站,解析搜索结果,并返回所有匹配的音乐信息。
def search_music(keyword, headers, cookies):
base_url = f'https://www.qeecc.com/so/{keyword}.html'
try:
response = requests.get(base_url, timeout=20, headers=headers, cookies=cookies)
response.raise_for_status()
root = etree.HTML(response.text)
page_num = root.xpath('//div[@class="page"]/a[4]/@href')
total_page = 1
if page_num:
page_str = page_num[0]
match = re.search(r'(\d+)\.html$', page_str)
total_page = int(match.group(1)) if match else 1
all_results = []
global_index = 1
for page in range(1, total_page + 1):
page_url = f'https://www.qeecc.com/so/{keyword}/{page}.html'
page_response = requests.get(page_url, headers=headers)
page_root = etree.HTML(page_response.text)
sing_names = page_root.xpath('//div[@class="play_list"]/ul/li/div[@class="name"]/a/text()')
music_urls = page_root.xpath('//div[@class="play_list"]/ul/li/div[@class="name"]/a/@href')
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)
return all_results
except RequestException as e:
print(f"请求失败: {e}")
return []
路由处理
现在,我们需要定义几个路由来处理用户的请求。
@app.route('/')
def index():
return render_template('index.html')
@app.route('/search', methods=['POST'])
def search():
try:
headers = {
'accept': 'application/json, text/javascript, */*; q=0.01',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
'dnt': '1',
'origin': 'https://www.qeecc.com',
'priority': 'u=1, i',
'referer': 'https://www.qeecc.com/song/ZGRjc2NuZ3hp.html',
'sec-ch-ua': '"Microsoft Edge";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'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',
'x-requested-with': 'XMLHttpRequest',
}
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'{int(time.time())}',
}
keyword = urllib.parse.quote(request.form['keyword'])
results = search_music(keyword, headers, cookies)
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():
try:
song_index = int(request.json.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]
res = requests.post(
'http://www.qeecc.com/js/play.php',
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'{int(time.time())}',
},
headers={
'User-Agent': choice(user_agents),
'Referer': f'https://www.qeecc.com/song/{music_id}.html',
},
data={'id': music_id, 'type': 'music'},
timeout=10
)
res.raise_for_status()
sing_data = res.json()
sing_url = sing_data['url'].replace('http://', 'https://')
return jsonify({
"url": sing_url,
"filename": f"{sing_name}.mp3"
})
except Exception as e:
app.logger.error(f'下载失败: {str(e)}')
return jsonify(success=False, error="下载失败,请检查控制台日志")
@app.route('/get_audio_url/<int:index>')
def get_audio_url(index):
if 'search_results' not in session:
return jsonify(error="请先搜索"), 403
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:
res = requests.post(
'http://www.qeecc.com/js/play.php',
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'{int(time.time())}',
},
headers={
'User-Agent': choice(user_agents),
'Referer': f'https://www.qeecc.com/song/{music_id}.html',
'Origin': 'https://www.qeecc.com'
},
data={'id': music_id, 'type': 'music'},
timeout=10
)
res.raise_for_status()
return jsonify(url=res.json()['url'].replace('http://', 'https://')), 200, {
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'no-cache'
}
except Exception as e:
app.logger.error(f'音频获取失败: {str(e)}')
return jsonify(error="无法获取音频"), 500
前端页面设计
最后,我们需要设计一个简单的前端页面来展示搜索结果并提供下载功能。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>音乐下载器</title>
<link rel="icon" href="data:;base64,=" >
<style>
.loader {
width: 12px;
height: 12px;
border: 2px solid #4CAF50;
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s linear infinite;
display:inline-block;
margin-left:8px;
}
@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);
}
button:hover {
background: #45a049;
transform: translateY(-1px);
}
.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;
}
#search-loading{
display:none;
margin:15px 0;
color:#666;
}
.loader{
}
</style>
</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>
</form>
<!-- 在搜索表单下方添加加载提示 -->
<div id="search-loading" >
⌛ 正在搜索中,请稍候...
<div class="loader">
</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">
{{ result[0].split('《')[1].split('》')[0] }}
</div>
<div class="song-info">
{{ result[0].split('《')[0] }}
</div>
</div>
<input type="hidden" class="song-index" value="{{ loop.index }}">
</div>
{% endfor %}
</div>
{% endif %}
<!-- 在播放器中添加加载状态 -->
<div id="global-player">
<div id="loading-status">
⌛ 加载中...
</div>
<audio controls id="main-player">
您的浏览器不支持音频播放
</audio>
</div>
<div id="download-status">
{% if message %}
<div class="alert">
{{ message }}
<button onclick="this.parentElement.style.display='none'">×</button>
</div>
{% endif %}
</div>
<script>
// 拦截表单提交事件
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 currentPlayer = document.getElementById('main-player');
let currentPlayingIndex = null;
// 更新播放函数
async function loadAudio(index) {
const statusDiv = document.getElementById('loading-status');
statusDiv.style.display = 'block';
try {
const response = await fetch(`/get_audio_url/${index}`);
const data = await response.json();
const player = document.getElementById('main-player');
player.src = data.url;
player.play();
currentSongIndex = index;
} catch (error) {
console.error('加载失败:', error);
showStatusMessage('播放失败: ' + error.message, 'error');
} finally {
statusDiv.style.display = 'none';
}
}
// 统一播放控制
// 添加 event 参数
currentPlayer.addEventListener('play', (event) => {
document.querySelectorAll('.play-button').forEach(btn => {
btn.textContent = '播放';
});
event.target.textContent = '暂停';
});
currentPlayer.addEventListener('pause', (event) => {
event.target.textContent = '播放';
});
// 改为通过 DOM 查找按钮元素
async function startDownload(button) {
const index = document.querySelector(`.music-card:nth-child(${currentSongIndex}) .song-index`).value;
try {
button.disabled = true;
button.textContent = '下载中...';
const response = await fetch('/download', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ song_index: index })
});
const data = await response.json();
if (data.url) {
// 直接创建链接触发下载
const link = document.createElement('a');
link.href = data.url;
link.download = data.filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showStatusMessage('下载已开始,请查看浏览器下载列表', 'success');
} else {
showStatusMessage(data.error || '下载失败', 'error');
}
} catch (error) {
showStatusMessage('网络错误: ' + error.message, 'error');
} finally {
button.disabled = false;
button.textContent = '下载';
}
}
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>
`;
}
let currentSongIndex = null;
function handlePlay(index) {
currentSongIndex = index;
const statusDiv = document.getElementById('loading-status');
statusDiv.style.display = 'block';
loadAudio(index).finally(() => {
statusDiv.style.display = 'none';
});
}
// 修复全局下载函数
async function handleGlobalDownload() {
if (!currentSongIndex) {
showStatusMessage('请先双击选择要下载的歌曲', 'warning');
return;
}
try {
const index = document.querySelector(`.music-card:nth-child(${currentSongIndex}) .song-index`).value;
await startDownload({ target: { disabled: false } }); // 传递模拟按钮对象
showStatusMessage('下载请求已发送', 'success');
} catch (error) {
showStatusMessage('下载失败: ' + error.message, 'error');
}
}
</script>
</body>
</html>
项目完整代码
后端的 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 os
import time
import re
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。"""
return {'User-Agent': choice(user_agents)}
def search_music(keyword, headers, cookies):
"""搜索音乐并返回所有分页结果"""
# 获取总页数逻辑
base_url = f'https://www.qeecc.com/so/{keyword}.html'
try:
response = requests.get(base_url, timeout=20, headers=headers, cookies=cookies)
response.raise_for_status()
root = etree.HTML(response.text)
# 分页判断逻辑
page_num = root.xpath('//div[@class="page"]/a[4]/@href')
total_page = 1
if page_num:
page_str = page_num[0]
match = re.search(r'(\d+)\.html$', page_str)
total_page = int(match.group(1)) if match else 1
# 分页采集逻辑
all_results = []
global_index = 1 # 全局序号计数器
for page in range(1, total_page + 1):
page_url = f'https://www.qeecc.com/so/{keyword}/{page}.html'
page_response = requests.get(page_url, headers=headers)
page_root = etree.HTML(page_response.text)
# 解析单页结果
sing_names = page_root.xpath('//div[@class="play_list"]/ul/li/div[@class="name"]/a/text()')
music_urls = page_root.xpath('//div[@class="play_list"]/ul/li/div[@class="name"]/a/@href')
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) # 添加请求间隔
return all_results
except RequestException as e:
print(f"请求失败: {e}")
return []
@app.route('/')
def index():
return render_template('index.html')
@app.route('/search', methods=['POST'])
def search():
try:
headers = {
'accept': 'application/json, text/javascript, */*; q=0.01',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
'dnt': '1',
'origin': 'https://www.qeecc.com',
'priority': 'u=1, i',
'referer': 'https://www.qeecc.com/song/ZGRjc2NuZ3hp.html',
'sec-ch-ua': '"Microsoft Edge";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'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',
'x-requested-with': 'XMLHttpRequest',
}
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}',
}
keyword = urllib.parse.quote(request.form['keyword']) # URL编码关键词
results = search_music(keyword, headers, cookies)
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():
try:
song_index = int(request.json.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]
# 获取音乐文件URL
res = requests.post(
'http://www.qeecc.com/js/play.php',
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': f'https://www.qeecc.com/song/{music_id}.html',
},
data={'id': music_id, 'type': 'music'},
timeout=10
)
res.raise_for_status()
sing_data = res.json()
sing_url = sing_data['url'].replace('http://', 'https://')
# 直接返回下载链接
return jsonify({
"url": sing_url,
"filename": f"{sing_name}.mp3"
})
except Exception as e:
app.logger.error(f'下载失败: {str(e)}')
return jsonify(success=False, error="下载失败,请检查控制台日志")
#使用服务器端会话存储方案
app.config['SESSION_TYPE'] = 'filesystem'
app.config['SESSION_FILE_DIR'] = './flask_session'
Session(app)
# get_audio路由处理逻辑
@app.route('/get_audio_url/<int:index>')
def get_audio_url(index):
if 'search_results' not in session:
return jsonify(error="请先搜索"), 403
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:
res = requests.post(
'http://www.qeecc.com/js/play.php',
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': f'https://www.qeecc.com/song/{music_id}.html',
'Origin': 'https://www.qeecc.com' # 新增Origin头
},
data={'id': music_id, 'type': 'music'},
timeout=10
)
res.raise_for_status()
return jsonify(url=res.json()['url'].replace('http://', 'https://')), 200, {
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'no-cache'
}
except Exception as e:
app.logger.error(f'音频获取失败: {str(e)}')
return jsonify(error="无法获取音频"), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
前端的 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,=" >
<style>
.loader {
width: 12px;
height: 12px;
border: 2px solid #4CAF50;
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s linear infinite;
display:inline-block;
margin-left:8px;
}
@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);
}
button:hover {
background: #45a049;
transform: translateY(-1px);
}
.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;
}
#search-loading{
display:none;
margin:15px 0;
color:#666;
}
.loader{
}
</style>
</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>
</form>
<!-- 在搜索表单下方添加加载提示 -->
<div id="search-loading" >
⌛ 正在搜索中,请稍候...
<div class="loader">
</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">
{{ result[0].split('《')[1].split('》')[0] }}
</div>
<div class="song-info">
{{ result[0].split('《')[0] }}
</div>
</div>
<input type="hidden" class="song-index" value="{{ loop.index }}">
</div>
{% endfor %}
</div>
{% endif %}
<!-- 在播放器中添加加载状态 -->
<div id="global-player">
<div id="loading-status">
⌛ 加载中...
</div>
<audio controls id="main-player">
您的浏览器不支持音频播放
</audio>
</div>
<div id="download-status">
{% if message %}
<div class="alert">
{{ message }}
<button onclick="this.parentElement.style.display='none'">×</button>
</div>
{% endif %}
</div>
<script>
// 拦截表单提交事件
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 currentPlayer = document.getElementById('main-player');
let currentPlayingIndex = null;
// 更新播放函数
async function loadAudio(index) {
const statusDiv = document.getElementById('loading-status');
statusDiv.style.display = 'block';
try {
const response = await fetch(`/get_audio_url/${index}`);
const data = await response.json();
const player = document.getElementById('main-player');
player.src = data.url;
player.play();
currentSongIndex = index;
} catch (error) {
console.error('加载失败:', error);
showStatusMessage('播放失败: ' + error.message, 'error');
} finally {
statusDiv.style.display = 'none';
}
}
// 统一播放控制
// 添加 event 参数
currentPlayer.addEventListener('play', (event) => {
document.querySelectorAll('.play-button').forEach(btn => {
btn.textContent = '播放';
});
event.target.textContent = '暂停';
});
currentPlayer.addEventListener('pause', (event) => {
event.target.textContent = '播放';
});
// 改为通过 DOM 查找按钮元素
async function startDownload(button) {
const index = document.querySelector(`.music-card:nth-child(${currentSongIndex}) .song-index`).value;
try {
button.disabled = true;
button.textContent = '下载中...';
const response = await fetch('/download', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ song_index: index })
});
const data = await response.json();
if (data.url) {
// 直接创建链接触发下载
const link = document.createElement('a');
link.href = data.url;
link.download = data.filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showStatusMessage('下载已开始,请查看浏览器下载列表', 'success');
} else {
showStatusMessage(data.error || '下载失败', 'error');
}
} catch (error) {
showStatusMessage('网络错误: ' + error.message, 'error');
} finally {
button.disabled = false;
button.textContent = '下载';
}
}
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>
`;
}
let currentSongIndex = null;
function handlePlay(index) {
currentSongIndex = index;
const statusDiv = document.getElementById('loading-status');
statusDiv.style.display = 'block';
loadAudio(index).finally(() => {
statusDiv.style.display = 'none';
});
}
// 修复全局下载函数
async function handleGlobalDownload() {
if (!currentSongIndex) {
showStatusMessage('请先双击选择要下载的歌曲', 'warning');
return;
}
try {
const index = document.querySelector(`.music-card:nth-child(${currentSongIndex}) .song-index`).value;
await startDownload({ target: { disabled: false } }); // 传递模拟按钮对象
showStatusMessage('下载请求已发送', 'success');
} catch (error) {
showStatusMessage('下载失败: ' + error.message, 'error');
}
}
</script>
</body>
</html>
运行应用
通过以下命令运行你的Flask应用:
python app.py
打开浏览器并访问http://127.0.0.1:5000/(具体访问地址以运行结果为准)
,你就可以开始使用这个音乐搜索与下载平台了。
运行结果展示
后端结果
前端展示
注:代码原创,文章由AI生成