[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 = 'AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAAgBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AT8f/wg/H/8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8//wRANvtLQTb5l0E2+89BNvrzQTf6/kE3+v5BNvrzQTb7z0E2+5dANvtLPz//BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoq/wZAN/pvQTb65EI3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9BNvrkQjb6cCoq/wYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCM/kyQTf71UI3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/QTb61kI4/zIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQTX8VkE2+vZCN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/QTb690A0/FcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE1/FZBNvr8Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/QTb6/EA0/FcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCM/kyQTb69kI3+/9CN/v/Qjf7/0E2+f9ANvf/QTb6/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/QTb690Iz/zIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKir/BkE1+tVCN/v/Qjf7/0E2+v9eV+D/nJjg/6Sg4/+Ggd7/RDrs/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/QTb61ioq/wYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABANfpuQjf7/0I3+/9CN/v/ST/3/+Tj/P/s6///7Ov//+zr//+sqPT/Qjf7/0I3+/9CN/v/QTb5/zwy4v9COd3/PDPg/0A29v9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjb6cAAAAAAAAAAAAAAAAAAAAAAAAAAAPwD/BEE1+uNCN/v/Qjf7/0I3+/9QRvv/6+r+/+zr///s6///7Ov//8zJ/f9CN/v/Qjf7/0I3+/9zbOf/3Nr4/+zr///i4fv/jojj/0E2+f9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9BNvrkPwD/BAAAAAAAAAAAAAAAAAAAAABCNPtJQjf7/0I3+/9CN/v/Qjf7/0I3+/+Qivz/09D+/97c/v/o5/7/zMn9/0I3+/9CN/v/Qjf7/8bD/P/s6///7Ov//+zr///r6v7/UUf5/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9BN/tKAAAAAAAAAAAAAAAAAAAAAEE3+5RCN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/2tj+//Myf3/Qjf7/0I3+/9CN/v/ran9/+zr///s6///7Ov//+zr//9XTfv/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I2+5YAAAAAAAAAAAAAAAAAAAAAQTf7zEI3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/XlX6/8zJ/f9CN/v/Qjf7/0I3+/9HPPv/gHn8/5aQ/P+hm/3/6ej+/1dN+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/QTb6zgAAAAAAAAAAAAAAAAAAAABBNvrwQjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9eVfr/zMn9/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+//T0f3/V037/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9BNvryAAAAAAAAAAAAAAAAAAD/A0E2+v5CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/15V+v/Myf3/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/9LP/f9XTfv/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0E3+v4qKv8GAAAAAAAAAAAAAP8DQTb6/kI3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/XlX6/8zJ/f9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/0s/9/1dN+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/QTf6/ioq/wYAAAAAAAAAAAAAAABCNvvvQjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9eVfr/zMn9/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+//Sz/3/V037/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9BN/vxAAAAAAAAAAAAAAAAAAAAAEE2+ctCN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/15V+v/Lyfn/PzXy/0E2+v9CN/v/Qjf7/0I3+/9CN/v/Qjf7/9LP/f9XTfv/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0E2+s0AAAAAAAAAAAAAAAAAAAAAQTX5k0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/XlX6/+jn/f+yruj/i4be/2dg3f9EO93/PDLn/0A19f9CN/v/0s/9/1dN+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/QTb5lQAAAAAAAAAAAAAAAAAAAABANftHQjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9eVfr/7Ov//+zr///s6///7Ov//+rp/v/OzPP/pqPk/4R+3v/f3fr/V037/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/8+NPtJAAAAAAAAAAAAAAAAAAAAAAAA/wNBNvrhQjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/15V+v/s6///7Ov//+zr///s6///7Ov//+zr///s6///7Ov//+zr//9XTfv/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/QTb74lUA/wMAAAAAAAAAAAAAAAAAAAAAAAAAAEA2+mtCN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/UEb7/83L/v/p6P7/7Ov//+zr///s6///7Ov//+zr///s6///7Ov//1dN+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9BNfptAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPz//BEE2+9JCN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0g9+/9mXvv/iIH8/6mk/f/Kx/7/5+b+/+zr///s6///V037/0I3+/9CN/v/Qjf7/0I3+/9CN/v/QDb61DMz/wUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQjf/LkE2+vVCN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9GO/v/Yln7/3py/P9DOfv/Qjf7/0I3+/9CN/v/Qjf7/0E2+vY/NfkwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQTT4UkI2+vtCN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CNvr7QDf7UwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQjX7UUE2+vVCN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/QTb69kA3+1MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQjf5LkE2+tJCN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0E2+tNBNvkvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPz//BEA2+mtBNvrhQjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0I3+/9CN/v/Qjf7/0E2++FANvxrMzP/BQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wNBNvtGQjb7kkE2+8pCNvrvQTb6/kE2+v5CNvvvQTb7y0E1+5NANftHAAD/AwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8CAAD/AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////8f///wAP//wAA//4AAH/8AAA/+AAAH/AAAA/gAAAH4AAAB8AAAAPAAAADwAAAA8AAAAPAAAADgAAAAYAAAAHAAAADwAAAA8AAAAPAAAADwAAAA+AAAAfgAAAH8AAAD/gAAB/8AAA//gAAf/8AAP//wAP///5///////8='
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', target='_mp3').text).strip().split('《')
_href = item.find('a', target='_mp3').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())