免费音乐搜索和下载工具

 佚名文
发布时间:2025-10-20 11:27

[Python] 纯文本查看 复制代码

import sys from PyQt6.QtWidgets import QApplication, QMessageBox, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView, QLabel from PyQt6.QtCore import QThread, pyqtSignal, Qt from PyQt6.QtGui import QKeySequence, QShortcut, QIcon from urllib.parse import quote from requests import Session from bs4 import BeautifulSoup as BS from html import unescape from functools import partial from json import loads, dumps from base64 import b64decode import os ICON_BASE64 = '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' SESSION = Session() HEADERS = { 'Referer': 'http://www.22a5.com/', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', } class DownloadWorker(QThread): finished = pyqtSignal(str) error = pyqtSignal(str) def __init__(self, id): super().__init__() self.id = id def run(self): try: r = SESSION.post('http://www.22a5.com/js/play.php', headers=HEADERS, data={ 'id': self.id, 'type': 'music', }, timeout=30).json() title = r.get('title') url = r.get('url') filename = title.split('[Mp3')[0] + '.mp3' if os.path.exists(filename): self.finished.emit(dumps({'status': True, 'filename': filename, 'id': self.id, 'msg': '下载成功'})) return response = SESSION.get(url, headers={ 'Range': 'bytes=0-', 'User-Agent': HEADERS.get('User-Agent'), }, timeout=60, stream=True) with open(filename, 'wb') as f: # 非流式下载 # f.write(response.content) for chunk in response.iter_content(chunk_size=8192): f.write(chunk) self.finished.emit(dumps({'status': True, 'filename': filename, 'id': self.id, 'msg': '下载成功'})) except Exception as e: self.error.emit(dumps({'status': False, 'id': self.id, 'msg': f'下载出错: {str(e)}'})) class MusicSearchApp(QWidget): def __init__(self): super().__init__() self.setWindowTitle('聚合音乐搜索1.0, by tony') self.setGeometry(100, 100, 600, 400) self.set_icon() self.worker = None self.search_list = [] self.wait_list = [] self.done_list = [] self.layout = QVBoxLayout() self.input_layout = QHBoxLayout() # Create a horizontal layout self.input_box = QLineEdit(self) self.input_box.setPlaceholderText('输入歌名、歌手或专辑搜索') self.input_layout.addWidget(self.input_box) # Add input box to horizontal layout self.enter_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Return), self.input_box) # 信号和槽函数关联 self.enter_shortcut.activated.connect(self.search_music) self.search_button = QPushButton('搜索', self) self.search_button.clicked.connect(self.search_music) self.input_layout.addWidget(self.search_button) # Add button to horizontal layout self.layout.addLayout(self.input_layout) # Add horizontal layout to main vertical layout self.results_table = QTableWidget(self) self.results_table.setColumnCount(3) self.results_table.setHorizontalHeaderLabels(['歌曲名', '歌手', '操作']) self.results_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) self.layout.addWidget(self.results_table) self.results_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) self.page_label = QLabel("1", self) self.pagination_layout = QHBoxLayout() self.prev_button = QPushButton("上一页", self) self.prev_button.clicked.connect(partial(self.update_results_table, 'prev')) self.pagination_layout.addWidget(self.prev_button) self.pagination_layout.addWidget(self.page_label, alignment=Qt.AlignmentFlag.AlignCenter) self.next_button = QPushButton("下一页", self) self.next_button.clicked.connect(partial(self.update_results_table, 'next')) self.pagination_layout.addWidget(self.next_button) self.layout.addLayout(self.pagination_layout) self.setLayout(self.layout) def set_icon(self): icon_name = '.music.ico' if not os.path.exists(icon_name): icon = open(icon_name, 'wb') icon.write(b64decode(ICON_BASE64)) icon.close() self.setWindowIcon(QIcon(icon_name)) def search_music(self): keyword = self.input_box.text().strip() if not keyword: return keyword = quote(keyword) self.search_list = None try: while self.search_list is None: r = SESSION.get(f'http://www.22a5.com/so/{keyword}.html', headers=HEADERS, timeout=30) soup = BS(r.text, 'html.parser') self.search_list = soup.find('div', class_='play_list') self.search_list = self.search_list.find('ul').find_all('li') self.update_results_table('init') except Exception as e: QMessageBox.critical(self, '错误', '搜索失败:重试一下') print(e) def update_results_table(self, direction='next'): page = int(self.page_label.text()) if direction == 'next': page += 1 elif direction == 'prev' : page -= 1 else: page = 1 if len(self.search_list) == 0: self.page_label.setText('1') self.results_table.setRowCount(0) if page < 1 or page > 7: return size = 10 offset = (page-1) * size if len(self.search_list[offset:offset+size]) == 0: return self.page_label.setText(str(page)) self.results_table.setRowCount(len(self.search_list[offset:offset+size])) for i, item in enumerate(self.search_list[offset:offset+size]): _title = unescape(item.find('a',).text).strip().split('《') _href = item.find('a',).get('href') self.results_table.setItem(i, 0, QTableWidgetItem(_title[1].strip('》'))) self.results_table.setItem(i, 1, QTableWidgetItem(_title[0])) button = QPushButton('下载') button.clicked.connect(partial(self.start_download, _href[5:-5])) self.results_table.setCellWidget(i, 2, button) def start_download(self, id): if id in self.done_list: QMessageBox.warning(self, '提示', '该歌曲已经下载过啦') return if id in self.wait_list: QMessageBox.warning(self, '提示', '该歌曲已经在下载队列中啦') return # 尝试在这里限制并发数量 if self.worker is None: # 可以加上正在下载标识 self.worker = DownloadWorker(id) self.worker.finished.connect(self.download_callback) self.worker.error.connect(self.download_callback) self.worker.start() else: # 可以加上等待下载标识 self.wait_list.append(id) # 下载回调, 如果等待列表还有任务, 则继续下载 def download_callback(self, data): data = loads(data) if data['status']: QMessageBox.information(self, '成功', f'已下载: {data["filename"]}') self.done_list.append(data['id']) else: QMessageBox.critical(self, '错误', f'{data["msg"]}, 将自动重试') self.wait_list.insert(0, data['id']) self.worker.quit() self.worker = None if self.wait_list: id = self.wait_list.pop(0) self.start_download(id) if __name__ == '__main__': # '打包: pyinstaller -F -w -i .music.ico .\music_downloader.py' app = QApplication(sys.argv) window = MusicSearchApp() window.show() sys.exit(app.exec())

首页
评论
分享
Top