feat: add NetEase desktop player source (1.2.2)
This commit is contained in:
1 parent
4d40b0bd6d
commit
1fccf2bed8
12 files changed
+2843
No files matched your search
+400
@@ -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))
|
||||
Reference in new issue
Block a user