构建一个基于Flask的音乐搜索与下载平台

在这篇博文中,我们将介绍如何使用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生成 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值