feat: add NetEase desktop player source (1.2.2)

This commit is contained in:
yuncishu committed 2026-10-31 22:13:24 +08:00
1 parent 4d40b0bd6d
commit 1fccf2bed8
12 files changed
+2843

No files matched your search

+400
View File
@@ -0,0 +1,400 @@
"""版本检查、自动下载与安装。"""
import hashlib
import os
import re
import subprocess
import sys
import tempfile
import urllib.parse
import requests
from PySide6.QtCore import Qt, QUrl, QTimer
from PySide6.QtGui import QColor, QDesktopServices
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
from PySide6.QtWidgets import (
QApplication, QDialog, QFrame, QHBoxLayout, QLabel, QProgressBar, QPushButton,
QVBoxLayout, QWidget, QGraphicsDropShadowEffect,
)
from version import APP_VERSION, UPDATE_ENDPOINT
from widgets import make_icon, icon_button
UPDATE_QSS = """
#updateCard { background:#fffefa; border:1px solid #e6e1da; border-radius:18px; }
QPushButton#iconBtn { background:transparent; border:none; border-radius:7px; outline:none; }
QPushButton#iconBtn:hover { background:#f2efea; }
QPushButton:focus { outline:none; }
#updateEyebrow { color:#e6493f; font-size:9px; font-weight:700; letter-spacing:1.7px; }
#updateTitle { color:#22211f; font-size:20px; font-weight:700; }
#updateSub { color:#8c877f; font-size:12px; }
#updateMeta { color:#aaa59c; font-size:10.5px; }
#updateNotesTitle { color:#8c877f; font-size:10px; font-weight:600; letter-spacing:1px; }
#updateNote { color:#5f5b55; font-size:12px; }
#updateStatus { color:#8c877f; font-size:11px; }
QProgressBar#updateProgress {
border:none; border-radius:3px; background:#eeeae4; height:6px; text-align:center;
color:transparent;
}
QProgressBar#updateProgress::chunk { background:#e6493f; border-radius:3px; }
QPushButton#updateLater {
background:transparent; border:1px solid #e0dcd5; border-radius:8px;
min-height:36px; color:#6d6962; font-size:12px; padding:0 18px;
}
QPushButton#updateLater:hover { background:#f3f0eb; color:#242322; }
QPushButton#updatePrimary {
background:#e6493f; border:none; border-radius:8px; min-height:36px;
color:#ffffff; font-size:12px; font-weight:600; padding:0 20px;
}
QPushButton#updatePrimary:hover { background:#d93e35; }
QPushButton#updatePrimary:disabled { background:#e8e4de; color:#aaa59c; }
QPushButton#updateLink {
background:transparent; border:none; color:#9b968e; font-size:10.5px;
text-align:left; padding:0;
}
QPushButton#updateLink:hover { color:#625f5a; }
"""
def parse_version(value):
text = str(value or "").strip()
text = re.split(r"[-+]", text, maxsplit=1)[0]
return tuple(int(part) for part in re.findall(r"\d+", text))
def is_newer(latest, current=APP_VERSION):
left, right = parse_version(latest), parse_version(current)
if not left:
return False
size = max(len(left), len(right))
return left + (0,) * (size - len(left)) > right + (0,) * (size - len(right))
def fetch_update_info(endpoint=UPDATE_ENDPOINT):
response = requests.get(
endpoint,
timeout=10,
headers={"User-Agent": "NeteasePlayer/%s" % APP_VERSION, "Cache-Control": "no-cache"},
)
response.raise_for_status()
info = response.json()
if not isinstance(info, dict):
raise RuntimeError("更新信息格式错误")
if not info.get("version") or not info.get("download_url"):
raise RuntimeError("更新信息缺少必要字段")
return info
def human_size(size):
try:
size = float(size)
except (TypeError, ValueError):
return ""
if size >= 1024 ** 3:
return "%.2f GB" % (size / 1024 ** 3)
if size >= 1024 ** 2:
return "%.1f MB" % (size / 1024 ** 2)
return "%.0f KB" % (size / 1024)
def _safe_suffix(url):
suffix = os.path.splitext(urllib.parse.urlparse(url).path)[1].lower()
return suffix if suffix in (".exe", ".msi", ".zip") else ".exe"
def _offset_dir(version):
base = os.environ.get("LOCALAPPDATA") or tempfile.gettempdir()
path = os.path.join(base, "NeteasePlayer", "updates")
os.makedirs(path, exist_ok=True)
return path
def _quote(path):
return '"' + str(path).replace('"', '""') + '"'
class UpdateDialog(QDialog):
"""发现新版本后立即自动下载,下载完成可一键重启安装。"""
def __init__(self, info, parent=None, on_state=None):
super().__init__(parent)
self.info = info
self.on_state = on_state
self.reply = None
self.file = None
self.ready_path = None
self.downloaded = 0
self.total = int(info.get("size") or 0)
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setModal(True)
self.setStyleSheet(UPDATE_QSS)
self._build_ui()
QTimer.singleShot(300, self.start_download)
def _build_ui(self):
outer = QVBoxLayout(self)
outer.setContentsMargins(18, 18, 18, 18)
card = QFrame(objectName="updateCard")
shadow = QGraphicsDropShadowEffect(card)
shadow.setBlurRadius(38)
shadow.setOffset(0, 12)
shadow.setColor(QColor(28, 27, 31, 65))
card.setGraphicsEffect(shadow)
outer.addWidget(card)
layout = QVBoxLayout(card)
layout.setContentsMargins(26, 22, 26, 22)
layout.setSpacing(0)
top = QHBoxLayout()
top.setSpacing(13)
icon_box = QLabel()
icon_box.setFixedSize(42, 42)
icon_box.setPixmap(make_icon("refresh", "#ffffff", 20).pixmap(42, 42))
icon_box.setStyleSheet("background:#e6493f; border-radius:12px;")
icon_box.setAlignment(Qt.AlignCenter)
top.addWidget(icon_box)
titles = QVBoxLayout()
titles.setSpacing(2)
titles.addWidget(QLabel("NEW VERSION", objectName="updateEyebrow"))
self.title = QLabel("发现新版本", objectName="updateTitle")
titles.addWidget(self.title)
top.addLayout(titles, 1)
close = icon_button("close", 32, 15, tooltip="稍后再说")
close.clicked.connect(self.hide)
top.addWidget(close)
layout.addLayout(top)
layout.addSpacing(16)
latest = self.info.get("version", "")
self.sub = QLabel("正在为你自动下载新版本", objectName="updateSub")
layout.addWidget(self.sub)
notes = self.info.get("notes") or []
date = self.info.get("release_date") or ""
size = human_size(self.info.get("size"))
meta_parts = ["v%s → v%s" % (APP_VERSION, latest)]
if date:
meta_parts.append(date)
if size:
meta_parts.append(size)
self.meta = QLabel(" · ".join(meta_parts), objectName="updateMeta")
layout.addWidget(self.meta)
layout.addSpacing(16)
notes_title = QLabel("本次更新", objectName="updateNotesTitle")
layout.addWidget(notes_title)
layout.addSpacing(7)
box = QWidget()
box_layout = QVBoxLayout(box)
box_layout.setContentsMargins(0, 0, 0, 0)
box_layout.setSpacing(6)
if not notes:
notes = ["稳定性与性能优化"]
for note in notes[:6]:
row = QHBoxLayout()
row.setSpacing(8)
dot = QLabel("·")
dot.setStyleSheet("color:#e6493f; font-size:16px; font-weight:700;")
dot.setFixedWidth(8)
item = QLabel(str(note), objectName="updateNote")
item.setWordWrap(True)
row.addWidget(dot, 0, Qt.AlignTop)
row.addWidget(item, 1)
box_layout.addLayout(row)
layout.addWidget(box)
layout.addSpacing(18)
self.progress = QProgressBar(objectName="updateProgress")
self.progress.setRange(0, 100)
self.progress.setValue(0)
self.progress.setTextVisible(False)
layout.addWidget(self.progress)
layout.addSpacing(7)
self.status = QLabel("准备下载…", objectName="updateStatus")
layout.addWidget(self.status)
layout.addSpacing(16)
footer = QHBoxLayout()
footer.setSpacing(9)
self.link = QPushButton("查看下载页", objectName="updateLink")
self.link.setCursor(Qt.PointingHandCursor)
self.link.clicked.connect(self._open_page)
footer.addWidget(self.link)
footer.addStretch()
self.later = QPushButton("稍后再说", objectName="updateLater")
self.later.setCursor(Qt.PointingHandCursor)
self.later.clicked.connect(self.hide)
footer.addWidget(self.later)
self.primary = QPushButton("正在下载…", objectName="updatePrimary")
self.primary.setCursor(Qt.PointingHandCursor)
self.primary.setEnabled(False)
self.primary.clicked.connect(self._primary_action)
footer.addWidget(self.primary)
layout.addLayout(footer)
def _open_page(self):
QDesktopServices.openUrl(QUrl(self.info.get("page_url") or "https://music.yuncishu.xyz/download/"))
def start_download(self):
if self.ready_path:
return
if self.reply:
self.reply.abort()
self.reply.deleteLater()
self.reply = None
if self.file:
try:
self.file.close()
except Exception:
pass
self.file = None
url = self.info.get("download_url")
filename = "NeteasePlayer-%s%s" % (self.info.get("version", "new"), _safe_suffix(url))
self.ready_path = os.path.join(_offset_dir(self.info.get("version", "new")), filename)
self.file = open(self.ready_path + ".part", "wb")
self.downloaded = 0
self.progress.setValue(0)
self.status.setText("正在连接下载服务器…")
self.primary.setText("正在下载…")
self.primary.setEnabled(False)
self._notify("downloading", "正在下载")
self.manager = QNetworkAccessManager(self)
request = QNetworkRequest(QUrl(url))
request.setRawHeader(b"User-Agent", ("NeteasePlayer/%s" % APP_VERSION).encode())
try:
request.setAttribute(
QNetworkRequest.Attribute.RedirectPolicyAttribute,
QNetworkRequest.RedirectPolicy.NoLessSafeRedirectPolicy,
)
except Exception:
pass
self.reply = self.manager.get(request)
self.reply.readyRead.connect(self._read)
self.reply.downloadProgress.connect(self._progress)
self.reply.finished.connect(self._finished)
def _read(self):
if not self.reply or not self.file:
return
data = bytes(self.reply.readAll())
if data:
self.file.write(data)
self.downloaded += len(data)
def _progress(self, received, total):
if total > 0:
self.total = total
percent = int(received * 100 / total)
self.progress.setValue(max(0, min(100, percent)))
self.status.setText("正在下载 %s / %s" % (human_size(received), human_size(total)))
else:
self.status.setText("正在下载 %s" % human_size(received))
def _finished(self):
reply = self.reply
if not reply:
return
self._read()
if self.file:
self.file.flush()
self.file.close()
self.file = None
if reply.error() != QNetworkReply.NetworkError.NoError:
self.status.setText("下载失败 · " + reply.errorString())
self.primary.setText("重新下载")
self.primary.setEnabled(True)
self.primary.setProperty("action", "retry")
self._notify("error", "更新下载失败")
reply.deleteLater()
self.reply = None
return
part_path = self.ready_path + ".part"
expected = int(self.info.get("size") or 0)
if expected and os.path.getsize(part_path) != expected:
self.status.setText("文件大小校验失败,请重新下载")
self.primary.setText("重新下载")
self.primary.setEnabled(True)
self.primary.setProperty("action", "retry")
self._notify("error", "更新文件不完整")
reply.deleteLater()
self.reply = None
return
digest = self.info.get("sha256")
if digest:
hasher = hashlib.sha256()
with open(part_path, "rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
hasher.update(chunk)
if hasher.hexdigest().lower() != str(digest).lower():
self.status.setText("安全校验失败,已阻止安装")
self.primary.setText("重新下载")
self.primary.setEnabled(True)
self.primary.setProperty("action", "retry")
self._notify("error", "更新文件校验失败")
reply.deleteLater()
self.reply = None
return
os.replace(part_path, self.ready_path)
self.ready_path_final = self.ready_path
self.progress.setValue(100)
self.status.setText("下载完成 · 已通过完整性校验")
self.sub.setText("新版本已准备好,重启后即可使用")
self.primary.setText("重启并安装")
self.primary.setEnabled(True)
self.primary.setProperty("action", "install")
self._notify("ready", "更新已就绪")
reply.deleteLater()
self.reply = None
def _primary_action(self):
action = self.primary.property("action")
if action == "retry":
self.primary.setProperty("action", "")
self.start_download()
elif action == "install":
self.install()
else:
self.install()
def install(self):
path = getattr(self, "ready_path_final", None) or self.ready_path
if not path or not os.path.exists(path):
self.status.setText("安装包不存在,请重新下载")
return
if not getattr(sys, "frozen", False):
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
self.status.setText("安装包已打开")
return
current = os.path.abspath(sys.executable)
script_path = os.path.join(tempfile.gettempdir(), "netease_player_update.cmd")
script = "\r\n".join([
"@echo off",
"chcp 65001 >nul",
"timeout /t 2 /nobreak >nul",
"taskkill /PID %d /F >nul 2>&1" % os.getpid(),
'copy /Y %s %s >nul 2>&1' % (_quote(path), _quote(current)),
'start "" %s' % _quote(current),
'del "%~f0" >nul 2>&1',
"",
])
with open(script_path, "w", encoding="utf-8") as handle:
handle.write(script)
subprocess.Popen(
["cmd.exe", "/c", script_path],
creationflags=0x08000000 | 0x00000008,
close_fds=True,
)
QApplication.quit()
def _notify(self, state, text):
if self.on_state:
self.on_state(state, text)
def showEvent(self, event):
super().showEvent(event)
parent = self.parentWidget()
if parent:
x = parent.x() + (parent.width() - self.width()) // 2
y = parent.y() + (parent.height() - self.height()) // 2
self.move(max(0, x), max(0, y))