From 1fccf2bed8a3aadcba8e6efc1f92503e77cdefb3 Mon Sep 17 00:00:00 2001
From: yuncishu <3053860096@qq.com>
Date: Sat, 31 Oct 2026 22:13:24 +0800
Subject: [PATCH] feat: add NetEase desktop player source (1.2.2)
---
.gitignore | 11 +
README.md | 56 ++
build.ps1 | 21 +
requirements.txt | 2 +
src/core.py | 161 ++++++
src/icon.ico | Bin 0 -> 46085 bytes
src/import_dialog.py | 104 ++++
src/main.py | 1298 ++++++++++++++++++++++++++++++++++++++++++
src/theme.py | 182 ++++++
src/updater.py | 400 +++++++++++++
src/version.py | 7 +
src/widgets.py | 601 +++++++++++++++++++
12 files changed, 2843 insertions(+)
create mode 100644 .gitignore
create mode 100644 build.ps1
create mode 100644 requirements.txt
create mode 100644 src/core.py
create mode 100644 src/icon.ico
create mode 100644 src/import_dialog.py
create mode 100644 src/main.py
create mode 100644 src/theme.py
create mode 100644 src/updater.py
create mode 100644 src/version.py
create mode 100644 src/widgets.py
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6ed534c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+__pycache__/
+*.py[cod]
+.venv/
+venv/
+build/
+dist/
+*.spec
+*.exe
+.env
+.DS_Store
+Thumbs.db
diff --git a/README.md b/README.md
index 4708af1..00210c8 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,58 @@
# music123-desktop
+网易云音乐 Windows 原生桌面播放器,基于 Python、PySide6(Qt 6)和 QMediaPlayer。
+
+本仓库只包含桌面版源码,不包含网页端、服务端配置或已构建的 exe。
+
+## 功能
+
+- 网易云官方排行榜,首页数据缓存,点击秒开
+- 歌曲搜索、最近播放、播放队列
+- 导入公开歌单:链接、分享文案或数字 ID
+- 标准 / 较高 / 极高 / 无损音质切换
+- 逐行歌词、精确居中、点击歌词跳转
+- 全屏播放页、黑胶旋转、音量与播放进度控制
+- 播放栏显示当前播放歌单
+- 应用内检查更新、自动下载、SHA-256 校验和重启安装
+
+## 开发运行
+
+要求 Python 3.11+,推荐 Python 3.13。
+
+```powershell
+python -m venv .venv
+.\.venv\Scripts\Activate.ps1
+pip install -r requirements.txt
+python src\main.py
+```
+
+## 构建 Windows exe
+
+```powershell
+pip install pyinstaller
+powershell -ExecutionPolicy Bypass -File .\build.ps1
+```
+
+产物位于 `dist\网易云播放器.exe`。
+
+## 目录
+
+```text
+src/
+ main.py 主窗口、播放状态与导航
+ widgets.py 黑胶、歌词、歌曲列表等原生组件
+ theme.py QSS 视觉系统
+ core.py 网易云接口封装、歌词解析
+ import_dialog.py 导入歌单弹窗
+ updater.py 版本检查、自动下载与安装
+ version.py 版本号与更新地址
+ icon.ico 应用图标
+```
+
+## 说明
+
+本项目为非官方客户端,仅供学习交流。音乐内容、商标及相关权利归网易云音乐及原始权利人所有。
+
+## 许可证
+
+GNU Affero General Public License v3.0,详见 [LICENSE](LICENSE)。
diff --git a/build.ps1 b/build.ps1
new file mode 100644
index 0000000..b906ef5
--- /dev/null
+++ b/build.ps1
@@ -0,0 +1,21 @@
+param(
+ [string]$Python = "python"
+)
+
+$ErrorActionPreference = "Stop"
+Set-Location $PSScriptRoot
+
+& $Python -m PyInstaller `
+ --noconfirm `
+ --clean `
+ --windowed `
+ --onefile `
+ --name "网易云播放器" `
+ --icon "src\icon.ico" `
+ --add-data "src\icon.ico;." `
+ --distpath "dist" `
+ --workpath "build" `
+ --specpath "build" `
+ "src\main.py"
+
+Write-Host "Built: dist\网易云播放器.exe"
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..4b5753b
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+PySide6>=6.12.0,<7
+requests>=2.34.0,<3
diff --git a/src/core.py b/src/core.py
new file mode 100644
index 0000000..a67edc5
--- /dev/null
+++ b/src/core.py
@@ -0,0 +1,161 @@
+"""网易云播放器 · 数据层:API 封装、歌词解析、格式化工具"""
+import re
+import urllib.parse
+
+import requests
+
+API = "https://music.yuncishu.xyz/api"
+
+QUALITIES = [
+ ("standard", "标准"),
+ ("higher", "较高"),
+ ("exhigh", "极高"),
+ ("lossless", "无损"),
+]
+QUALITY_BR = {"standard": 128, "higher": 192, "exhigh": 320, "lossless": 2000}
+QUALITY_LABEL = dict(QUALITIES)
+
+CHARTS = [
+ (19723756, "飙升榜", "每天更新"),
+ (3778678, "热歌榜", "每周四更新"),
+ (3779629, "新歌榜", "每天更新"),
+ (2884035, "原创榜", "每周四更新"),
+]
+
+
+def http_get(path, **params):
+ r = requests.get(API + path, params=params, timeout=20)
+ j = r.json()
+ if isinstance(j, dict) and j.get("code") not in (None, 200):
+ raise RuntimeError("接口返回 code=%s" % j.get("code"))
+ return j
+
+
+def https(url):
+ return re.sub(r"^http://", "https://", url or "", flags=re.I)
+
+
+def cover_of(song, size=200):
+ pic = ((song.get("al") or {}).get("picUrl")) or ""
+ return "%s?param=%dy%d" % (https(pic), size, size) if pic else ""
+
+
+def artists_of(song):
+ return " / ".join(a.get("name", "") for a in (song.get("ar") or []))
+
+
+def album_of(song):
+ return (song.get("al") or {}).get("name") or ""
+
+
+def is_vip(song):
+ return song.get("fee") in (1, 4) and song.get("st") != 0
+
+
+def fmt_ms(ms):
+ s = max(0, int((ms or 0) / 1000))
+ return "%02d:%02d" % (s // 60, s % 60)
+
+
+def search_songs(keyword, limit=50):
+ j = http_get("/cloudsearch", keywords=keyword, limit=limit, type=1)
+ return (j.get("result") or {}).get("songs") or []
+
+
+def fetch_playlist(pid):
+ return http_get("/playlist/detail", id=pid).get("playlist") or {}
+
+
+def parse_playlist_ref(text):
+ """从公开歌单链接、分享文案或数字 ID 中解析歌单 ID。"""
+ value = (text or "").strip()
+ if not value:
+ raise ValueError("请输入歌单链接或数字 ID")
+ if re.fullmatch(r"\d{4,}", value):
+ return int(value)
+ urls = re.findall(r"https?://[^\s]+", value)
+ target = urls[0] if urls else value
+ parsed = urllib.parse.urlparse(target)
+ host = (parsed.hostname or "").lower()
+ allowed = ("music.163.com", "y.music.163.com", "163cn.tv", "www.163cn.tv")
+ if host and host not in allowed:
+ raise ValueError("只支持网易云音乐的公开歌单链接")
+ match = re.search(r"[?]id=(\d+)", target)
+ if match:
+ return int(match.group(1))
+ match = re.search(r"/playlist/(\d+)", target)
+ if match:
+ return int(match.group(1))
+ if target.startswith("http"):
+ if host not in allowed:
+ raise ValueError("链接域名不是网易云音乐")
+ response = requests.get(
+ target, timeout=12, allow_redirects=True,
+ headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) NeteasePlayer"},
+ )
+ final = response.url
+ match = re.search(r"[?]id=(\d+)", final) or re.search(r"/playlist/(\d+)", final)
+ if match:
+ return int(match.group(1))
+ raise ValueError("没有从链接中找到歌单 ID,请粘贴完整的网易云歌单链接")
+
+
+def playlist_songs(pid, limit=100):
+ """取榜单/歌单歌曲(优先 tracks,缺失时用 trackIds 补)"""
+ pl = fetch_playlist(pid)
+ songs = (pl.get("tracks") or [])[:limit]
+ if len(songs) < 5:
+ ids = [t["id"] for t in (pl.get("trackIds") or [])[:limit]]
+ if ids:
+ j = http_get("/song/detail", ids=",".join(str(i) for i in ids))
+ songs = j.get("songs") or []
+ return pl, songs
+
+
+def song_play_url(song_id, level):
+ """返回可播放地址;原生无直链时回退解灰通道"""
+ try:
+ j = http_get("/song/url/v1", id=song_id, level=level)
+ data = j.get("data") or []
+ if data and data[0].get("url"):
+ return https(data[0]["url"]), 320
+ except Exception:
+ pass
+ return "%s/meting/?type=url&id=%s&br=%s" % (API, song_id, QUALITY_BR.get(level, 320)), 0
+
+
+def fetch_lyric(song_id):
+ try:
+ j = http_get("/lyric", id=song_id)
+ except Exception:
+ return []
+ return parse_lrc((j.get("lrc") or {}).get("lyric") or "", (j.get("tlyric") or {}).get("lyric") or "")
+
+
+def parse_lrc(lrc, tlyric=""):
+ def parse(text):
+ out = []
+ for raw in (text or "").splitlines():
+ m = re.match(r"^((?:\[\d+:\d+(?:[.:]\d+)?\])+)(.*)$", raw)
+ if not m:
+ continue
+ content = m.group(2).strip()
+ if not content:
+ continue
+ for st in re.finditer(r"\[(\d+):(\d+(?:[.:]\d+)?)\]", m.group(1)):
+ t = int(st.group(1)) * 60 + float(st.group(2).replace(":", "."))
+ out.append({"t": t, "text": content, "tr": ""})
+ out.sort(key=lambda x: x["t"])
+ return out
+
+ lines = parse(lrc)
+ trans = parse(tlyric)
+ if trans:
+ ti = 0
+ for line in lines:
+ while ti < len(trans) and trans[ti]["t"] < line["t"] - 0.4:
+ ti += 1
+ if ti < len(trans) and abs(trans[ti]["t"] - line["t"]) <= 0.4:
+ line["tr"] = trans[ti]["text"]
+ ti += 1
+ return lines
diff --git a/src/icon.ico b/src/icon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..e9e9e8c5c1aa71399fa999efddf661fa1122a143
GIT binary patch
literal 46085
zcmafab8u$C^7b1W8*8HtH@2M(-q`lWww;aj#
)U(pzhBjFst$U3s%Or0
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