# 飞鱼网盘音乐播放器 **Repository Path**: sumoy/FyMusic ## Basic Information - **Project Name**: 飞鱼网盘音乐播放器 - **Description**: 飞鱼网盘音乐播放器 - **Primary Language**: Unknown - **License**: EPL-1.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-29 - **Last Updated**: 2026-10-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README 简体中文 | [English](README.en.md) # 余音(Yuyin)设计与实施规划 > 余音不绝,绕梁三日。 > > 目标:在 Windows 上用 Python + PyQt6 做一个"多网盘音乐播放器"。 > 以 Chromium 内核(QtWebEngine)承载 115网盘 / 百度网盘 / 天翼云盘 / 移动云盘的网页, > 作为登录与文件操作入口;另加一套独立的播放器操作界面,登录后直接浏览、搜索、播放网盘里的音频。 > 在此之上提供 115「我听」听单、本地歌单、离线缓存、飞鱼会员云备份,以及基于作品页的在线更新。 > > 关键决策已确认(见第 11 节)。本文档既作为设计说明,也作为实施与验收依据。 --- ## 1. 项目定位 | 项目 | 内容 | | --- | --- | | 名称 | 余音(Yuyin) | | 宣传语 | 余音不绝,绕梁三日 | | 运行平台 | Windows 10/11 x64(先只做 Windows,跨平台留后) | | 运行方式 | 本地 Python 进程 + 内嵌 Chromium 视图,非 Electron、非浏览器插件 | | 网盘来源 | 115网盘、百度网盘、天翼云盘、移动云盘;115 额外支持「我听」听单 | | 核心体验 | 左边"我的播放器",右边所选网盘的网页;网页里登录一次,播放器里即可浏览目录、搜索、播放该账号网盘中的音频,并可建本地歌单 | | 附加能力 | 离线缓存与缓存管理、飞鱼会员云备份(歌单库 + 网盘登录态)、作品页在线更新(含更新说明、暂停/继续/停止) | | 内容边界 | 只播放当前账号自己网盘里的音频文件;不破解 VIP、不绕过限速、不接入第三方音乐源 | ### 1.1 要解决的问题 1. 各家网盘的网页版都没有面向"听歌"的顺手界面,切歌、调进度、看列表都很别扭。 2. 网盘直链有鉴权、有时效,普通播放器无法直接使用。 3. 希望登录流程完全复用网盘官方网页,不在本地保存账号密码。 4. 115「我听」里已有的听单和音频元数据是现成的音乐组织方式,希望直接出现在播放器里,而不是只能在网页里点播。 5. 网络不稳或想离线时,常听的歌能缓存到本地;换机或重装后,歌单与登录状态希望能一键恢复。 ### 1.2 非目标(本期不做) - 不做多账号切换、云盘文件上传/删除/重命名等管理功能。 - 不做在线音乐搜索、MV、直播;不做"另存/导出到用户指定目录"的下载功能(只做应用内离线缓存)。 - 不做移动端、Web 端。 - 不修改 115 网页本身的 DOM 结构(只做最小化的只读注入,见 5.9,且为可选项)。 --- ## 2. 关键技术判断 ### 2.1 "115 浏览器为内核"怎么落地 115 浏览器本身基于 Chromium(官网描述为"全新 Chromium 高速内核引擎")。因此有两条路线: | 方案 | 做法 | 优点 | 风险 | 结论 | | --- | --- | --- | --- | --- | | **A. QtWebEngine 内嵌(推荐)** | 用 PyQt6-WebEngine 的 `QWebEngineView` 加载 `115.com`,使用持久化 Profile 保存登录 Cookie,再从 Cookie 仓取会话给 Python 侧调用 WebAPI | 一体化窗口、可控、无需安装 115 浏览器、Cookie 可取 | Chromium 版本与 115 官网兼容性需实测;无官方"115 内核" | **主方案(已确认)** | | B. 接管本机 115 浏览器(CDP) | 用 `--remote-debugging-port` 启动 115 浏览器,通过 CDP 接管标签页并导出 Cookie | 与官方浏览器行为完全一致,风控风险最低 | 依赖用户已安装 115 浏览器、进程管理复杂、Cookie 导出受限 | **仅作兜底** | 设计上把"浏览器内核"抽象成一个接口(`BrowserKernel`),A/B 两种实现可替换,避免后续返工。 ### 2.2 播放链路判断 `QMediaPlayer` 只能接受一个 URL,而 115 的播放地址需要 Cookie/UA 且几分钟就过期。 所以采用 **本地代理 + 直链** 的方式: ``` QMediaPlayer → http://127.0.0.1:17890/stream/ → 本地代理(补 Cookie/UA、透传 Range) → 115 CDN 直链 ``` 好处:播放器拿到的是稳定 URL,可拖动进度、可预加载下一首;Cookie 不出现在播放器里; 直链过期时由代理自动换新地址。 --- ## 3. 技术栈 | 层 | 选型 | 说明 | | --- | --- | --- | | 语言 | Python 3.11.9 | 已建虚拟环境 `.venv` | | GUI | PyQt6 6.11.0(Qt 6.11.0) | 主窗口、控件、主题 | | 浏览器内核 | PyQt6-WebEngine 6.11.0,Chromium **140.0.7339.225**(已实测) | 登录 115、浏览网盘 | | 网络 | requests | 调 115 WebAPI、代理转发 | | 本地代理 | 标准库 `http.server` + `ThreadingHTTPServer`(或 `socketserver`) | 零额外依赖,够用 | | 音频播放(主) | QtMultimedia 的 `QMediaPlayer` + `QAudioOutput` | 系统解码器(WMF),覆盖 mp3/flac/wav/m4a | | 音频播放(兜底) | 外部 `mpv.exe` + 命名管道 JSON IPC | 提前落地;覆盖 ape/ogg/opus/wma/dsf 等;放 `third_party/mpv/` | | 离线缓存 | 文件系统 + SQLite 索引 | Range 断点续传、LRU 上限、优先本地播放 | | 标签/封面 | mutagen | 读取 ID3/FLAC 标签、内嵌封面、内嵌歌词 | | 本地存储 | SQLite(标准库 `sqlite3`) | 目录/我听缓存、歌单、播放历史、缓存索引 | | 打包(后期) | PyInstaller + mpv.exe | M6 阶段再处理 | 已执行的环境准备(已安装完成并验证): ```powershell python -m venv .venv .\.venv\Scripts\python.exe -m pip install -i https://mirrors.ustc.edu.cn/pypi/web/simple PyQt6 PyQt6-WebEngine requests mutagen ``` 实测版本:PyQt6 6.11.0 / Qt 6.11.0 / PyQt6-WebEngine 6.11.0(Chromium 140.0.7339.225)/ requests 2.34.2 / mutagen 1.48.1。 > 网络备注:清华与阿里云镜像在本机下载 Qt 运行时时速度偏低且易中断,改用中科大镜像后正常,后续装包默认用 USTC。 依赖清单后续落到 `requirements.txt`(M0 阶段)。 --- ## 4. 总体架构 ``` ┌────────────────────────────────────────────────────────────────────────┐ │ 余音 主窗口(QQ 音乐风格) │ │ ┌──────────┐ ┌─────────────────────────────────┐ ┌──────────────────┐ │ │ │ 侧边导航 │ │ 内容区 │ │ 播放队列抽屉 │ │ │ │ 发现音乐 │ │ · 歌单卡片 / 歌曲列表 │ │ (可收起) │ │ │ │ 我的音乐 │ │ · 我听听单 / 缓存管理 │ │ │ │ │ │ 我听 │ │ │ │ │ │ │ │ 本地缓存 │ │ │ │ │ │ │ └──────────┘ └─────────────────────────────────┘ └──────────────────┘ │ │ ┌────────────────────────────────────────────────────────────────────┐ │ │ │ 底部播放条:封面 / 标题 / 进度 / 控制 / 音量 / 模式 / 队列 │ │ │ └────────────────────────────────────────────────────────────────────┘ │ │ 右侧可展开:QWebEngineView(115 网页:登录 / 我听 / 文件管理) │ └───────────┬──────────────────┬───────────────────┬────────────────────┘ │ │ │ ▼ ▼ ▼ ┌────────────────┐ ┌────────────────┐ ┌──────────────────┐ │ PlayerService │ │ SessionService │ │ LibraryService │ │ 队列 / 状态机 │ │ Cookie 桥 │ │ 目录 / 我听索引 │ └───┬────────┬───┘ └───────┬────────┘ └────────┬─────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌───────────────┐ ┌──────────────┐ │QtBackend │ │MpvBackend│ │Cloud115Client │ │ CacheService │ │QMediaPlayer││mpv + IPC │ │files / 我听 │ │ 离线文件+LRU │ └────┬─────┘ └────┬─────┘ └───────┬───────┘ └──────┬───────┘ │ │ │ │ └────────────┴──────► LocalStreamProxy ◄──────┘ 127.0.0.1:17890 │ ▼ 115 CDN 直链 ``` 分层原则: - `ui/` 只处理界面与交互,不写 115 协议。 - `core/` 负责 115 会话、API、代理、我听、缓存、本地库,全部同步逻辑,独立可测。 - `player/` 用 `PlaybackBackend` 统一 Qt 与 mpv 两种后端,UI 只订阅状态信号。 - 网络/播放/下载类阻塞操作放到 `QThreadPool`/工作线程,主线程不阻塞。 --- ## 5. 关键机制设计 ### 5.1 登录态(SessionService) 1. 使用独立持久化 Profile(`QWebEngineProfile("yuyin")`), `setPersistentCookiesPolicy(ForcePersistentCookies)`,登录 Cookie 由 Chromium 自己保存与携带, 关掉程序再开仍是登录状态。 2. **不导出 Cookie**:实测 115 的 `webapi.115.com` 有 WAF,requests 直连返回 405 拦截页, 因此所有 WebAPI 调用改为页面内 `fetch`(见 5.2),HttpOnly Cookie 由浏览器自动附带。 3. 登录判定:页面加载完成后调用 `/files?aid=1&cid=0&limit=1`, 返回 `state:true` 即已登录;`/user/info` 可拿到脱敏账号。 4. 触发时机:页面 `loadFinished` + 关键 Cookie(UID/CID/SEID/KID)变化(700ms 防抖); 失败自动重试 3 次(2s/4s/6s),避免一次网络抖动就误判。 5. 不保存账号密码;Cookie 由 Qt 的加密存储管理。 6. UI:侧栏与顶栏显示「未登录/已登录」状态,115 面板右上角显示详细提示。 ### 5.2 请求通道(PageApiBridge) - 所有 115 WebAPI 通过 `QWebEnginePage.runJavaScript` 在页面内 `fetch`, 结果写入 `window.__fyResults` 后由 Python 轮询取回(Qt 的 `runJavaScript` 不等待 Promise)。 - 优势:自动携带 HttpOnly Cookie、复用 Chromium 的 DNS/代理/指纹,绕过 WAF。 - 统一使用 `credentials: "include"`、`cache: "no-store"`,15 秒超时。 - UA 仅用于内嵌网页(`profile.setHttpUserAgent`);M3 走 CDN 下载时沿用同一 UA。 - 代价:接口调用依赖页面存活;页面异常时提示「请刷新 115 页面」。 ### 5.3 音乐库(Cloud115Client + LibraryService) - 目录列表:`GET https://webapi.115.com/files`,常用参数 `aid=1`、`cid=<目录ID>`、`offset`、`limit`(分页)、`show_dir=1`、`o=user_ptime`、`asc=0`、`format=json`。 - 文件搜索:`GET https://webapi.115.com/files/search`(参数以抓包校准为准),作为"全盘搜索"能力。 - 字段映射(以实际返回为准,M1 阶段抓包核对): | 115 字段 | 含义 | 本地用途 | | --- | --- | --- | | `cid` | 目录 ID | 目录树节点 | | `fid` | 文件 ID | 主键之一 | | `pc` | pickcode | 取直链的关键 | | `n` / `name` | 名称 | 展示、匹配歌词 | | `s` / `size` | 大小 | 展示 | | `sha` | SHA1 | 可选去重 | | `ptime` | 修改时间 | 排序 | - 音频识别:不依赖 115 的类型筛选参数(`type` 取值需实测),先取全量再用扩展名白名单过滤: `mp3, flac, wav, m4a, aac, ogg, opus, wma, ape, dsf, dff, aiff`。 - 缓存策略:SQLite 保存 `folders` / `tracks`,进入目录时先渲染缓存再后台刷新; 目录懒加载(展开时才拉子目录)。 - 音量归一化、封面、时长:时长从 `QMediaPlayer.duration` 或 mutagen 读,写入缓存。 ### 5.4 「我听」听单(ListenService) 115「我听」入口为 `https://115.com/?ct=music`(网盘左侧「我听」直接进入)。 接口已在 M2 通过内嵌页面的网络请求实测确认(见附录 A): 1. **听单列表**:`GET /files/musicnew?topic_id=1&type=1&user_id=&start=0&limit=20` → `data.list[]`,字段 `topic_id`、`topic_name`(可能为空串)、`music_count`、`pic`、`topic_sort`; 响应里的 `curr_user.user_id` 会回写本地,供下次请求使用。 返回空列表时用 `/files/music_topic_listnew?fond=1&start=0&limit=20` 兜底。 2. **听单曲目**:`GET /files/music_list?topic_id=&start=0&limit=1150` → `data[]`,字段 `id`、`file_name`、`pick_code`、`file_size`、`play_long`(秒)、`url`(内置播放地址)。 3. **本地映射**:转换成统一曲目模型(`pickcode` / `name` / `size` / `duration`), 写入 `listen_sheets`、`listen_items` 表,先渲染缓存再后台刷新。 4. **播放**:我听条目本质仍是网盘音频文件,统一走 5.5 取直链 + 5.6 代理; 条目自带的 `url`(`/files/music?pickcode=…&topic_id=…&music_id=…`)作为 M3 的备选播放通道。 5. **进度续播**:接口若提供跨端进度则读写;否则用本地 `resume` 表按条目 ID 记录。 6. **降级**:接口不可用时,在页面内执行只读 JS 读取渲染后的数据(或 `window` 上的初始状态对象), 通过 `QWebChannel` 回传;不改页面逻辑,站点改版时可直接关闭。 ### 5.5 取播放地址(PlayUrlResolver) - 接口:页面内 `fetch` 调 `GET https://webapi.115.com/files/download?pickcode=` → 返回 JSON 中的 `file_url`(CDN 直链,带签名、短时效)。 - 缓存:`pickcode → (url, expire_at)`,TTL 取 5 分钟;播放中收到 403/404 立即失效并重取一次。 - 备选:115 开放平台的视频/音频播放接口(`/files/play`、`/open/play` 一类,需 VIP 能力时再评估)。 - 已缓存到本地的曲目不再取直链,直接给播放器本地文件路径。 - CDN 直链已实测:`/files/download` 返回 `file_url` / `file_size` / `sha1`;requests 带 Cookie + Range 可直接拿到 `206`(M3 已验证)。 - 关键点:CDN 鉴权依赖 `/files/download` 响应下发的 **path 限定 token Cookie** (形如 `<32位hex>=`,`Path=/<存储路径hash>/`),必须按路径匹配后随请求带上。 - 端点与参数在 M1 阶段已确认 `/files`、`/user/info` 可用。 ### 5.6 本地流媒体代理(LocalStreamProxy) - 仅监听 `127.0.0.1:17890`,路由:`GET /stream/`,token 为 `pickcode` 的不可预测映射(`secrets.token_urlsafe`)。 - 行为: - 透传客户端 `Range`,向 115 CDN 发起带 Cookie/UA 的请求; - 返回 `206 Partial Content` + `Content-Range` / `Content-Length` / `Content-Type` / `Accept-Ranges: bytes`; - 64KB 分块转发,不整文件进内存; - 支持连接中断清理、单曲最多 2 个并发连接(防拖拽风暴)。 - 与缓存共用同一套"取直链 + 带鉴权请求"代码,避免两处维护。 - Cookie 来源:Chromium 运行时会独占锁定 `profile/Cookies`,磁盘读不到;改为监听 `QWebEngineCookieStore.cookieAdded` 在内存维护 Cookie 罐(含 HttpOnly 与 path 限定项), 代理按请求 URL 的 host + path 匹配出 Cookie 头(M3 已验证)。 - 上游返回 403 时等待 0.6s 重取 Cookie 再试一次(覆盖 token 下发与播放请求的竞态)。 - 客户端主动断开(含 mpv 探测/拖动产生的短连接)按正常现象处理,只记 debug 日志,不抛异常。 - 安全:只接受本机回环地址;进程退出即关闭端口。 ### 5.7 离线缓存(CacheService) - 目标:已缓存曲目秒开、断网可听;只做应用内缓存,不做"导出/另存"。 - 目录:`<程序目录>\Cache\.`(默认位置,可在「本地缓存」页修改);下载中写 `.part`,完成后原子重命名。 - 下载:独立工作线程 + 队列(并发 2),复用 Range 逻辑,支持断点续传、失败重试 3 次。 - 触发: - 手动:播放条上的"缓存"按钮; - 自动(默认关闭,可在设置里打开):播放时后台缓存当前曲目,下次播放直接命中本地文件。 - 索引:`cache_files` 表记录 pickcode、路径、大小、状态、最近使用时间。 - 淘汰:总上限默认 10GB(可配),LRU 清理;正在播放/下载的条目不清理。 - 校验:完成后比对文件大小;有 `sha1` 时做完整性校验(大文件可延后到空闲时)。 - UI:列表显示"已缓存/缓存中"标记;侧栏"本地缓存"页展示占用、上限、一键清理。 - 实现(M5):`app/core/cache.py` 用 `threading` 工作线程并发 2 下载,进度/完成事件经 `task_changed(pickcode)` / `cache_changed` 回主线程,SQLite 只在主线程读写; `_done` 集合供曲目表/播放条做 O(1) 状态查询;播放命中缓存时 Qt 后端把本地路径转成 `file://` URL 再交给 QMediaPlayer。自动缓存开关与上限(1–500GB)写 `meta` 表。 - 实测(M5):`849.好心分手 - 卢巧音.mp3`(7.2MB)缓存到 `cache/audio/.mp3`, 再次播放直接命中本地文件(`backend=qt`、无网络请求);`.part` 断点续传与 LRU 淘汰随之生效。 ### 5.8 播放器与双后端(PlayerService) - 统一接口 `PlaybackBackend`:`load / play / pause / stop / seek / set_volume`, 以及 `position_changed / duration_changed / state_changed / finished / error` 信号;UI 只依赖接口。 - `QtBackend`:`QMediaPlayer` + `QAudioOutput`(Qt 6.11 自带 FFmpeg 后端),默认后端,启动快、系统解码。 - `MpvBackend`(M4 已落地):每个实例启动独立外部进程 `mpv.exe --idle=yes --no-video --audio-display=no --no-terminal --really-quiet --input-ipc-server=\\.\pipe\yuyin-mpv-<随机后缀> --volume=`; 用 `QLocalSocket` 连接(不带 `\\.\pipe\` 前缀,80ms × 最多 40 次重试), 连上后 `observe_property` 订阅 `time-pos / duration / pause`,通过 `loadfile ... replace` 起播、`set_property pause` 暂停、`seek ... absolute` 拖动;`file-loaded` / `end-file` (`eof`→`finished`、`error`→`error`)映射成统一信号。进程以 `CREATE_NO_WINDOW` 静默启动, 退出时先发 `quit` 再 `terminate/kill`,不留后台进程。 - 选择策略:`ape/ogg/opus/wma/dsf/dff/aiff` 直接走 mpv(`MPV_EXTS`); Qt 解码失败且未切换过后端时,自动切 mpv 并把当前进度作为 `_pending_seek` 续上, 同时发出 `state=fallback` 供播放条提示;mpv 也失败才换链重试一次,仍失败则提示。 - 状态机:`Idle → Resolving → Loading → Playing ⇄ Paused → Ended/Error`, 另有 `fallback`(Qt→mpv)与 `reconnecting`(换链重试)两个提示态。 - 队列:顺序 / 单曲循环 / 列表循环 / 随机。 - 失败重试:解析失败重试 1 次;播放中途直链失效换链并恢复进度 1 次,再失败则提示。 - 实测(M4):mpv v0.41.0-1227-ge471496c5(shinchiro 20261021 构建,仅保留 `mpv.exe`); 云端 `袁咏琳;周杰伦 - 画沙.ape`(29MB / 04:33)经本地代理直接播放, `backend=mpv`、`duration=273666`、`pos` 正常前进;Qt 遇损坏文件自动切 mpv 并发出 `fallback`。 - 分发注意:mpv 为 GPLv2+,随包分发需附许可证与源码获取方式;MVP 阶段支持用户自行放置 `mpv.exe`。 ### 5.9 浏览器区增强(可选,M6) - 在 115 网页里注入一个悬浮小按钮("加入播放列表"),通过 `QWebChannel` 与 Python 通信; 仅监听点击,不改动 115 原有 DOM 逻辑,站点改版时可直接关闭该功能。 - 页面 `title`/`urlChanged` 变化时同步地址栏与登录状态。 ### 5.10 数据模型(SQLite,`<程序目录>\Data\yuyin.db`) ```sql -- M2 已落地(app/core/store.py) folders(cid TEXT PRIMARY KEY, parent_cid TEXT, name TEXT, updated_at INTEGER); tracks(pickcode TEXT PRIMARY KEY, cid TEXT, fid TEXT, name TEXT, size INTEGER, mtime INTEGER, path TEXT, updated_at INTEGER); listen_sheets(topic_id TEXT PRIMARY KEY, name TEXT, pic TEXT, music_count INTEGER, sort INTEGER, updated_at INTEGER); listen_items(item_id TEXT PRIMARY KEY, topic_id TEXT, name TEXT, pickcode TEXT, size INTEGER, duration INTEGER, updated_at INTEGER); meta(key TEXT PRIMARY KEY, value TEXT); -- user_id、设置、上次选中的目录、音量等 -- M5 已落地(app/core/store.py) cache_files(pickcode TEXT PRIMARY KEY, path TEXT, size INTEGER, state TEXT, created_at INTEGER, last_used_at INTEGER); -- M6 预留 playlists(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, created_at INTEGER); playlist_items(playlist_id INTEGER, pickcode TEXT, sort INTEGER, PRIMARY KEY(playlist_id, pickcode)); history(pickcode TEXT PRIMARY KEY, played_at INTEGER, play_count INTEGER); resume(pickcode TEXT PRIMARY KEY, position_ms INTEGER, updated_at INTEGER); ``` ### 5.11 顶部菜单与快捷键 - 菜单栏:文件(搜索歌曲、显示/隐藏 115 面板、清空本地缓存、退出)、播放(播放/暂停、 上一首、下一首、音量 ±)、设置(快捷键设置…、播放时自动缓存、飞鱼会员 / 云备份…)、 帮助(会员空间、检查更新、官网、关于)。 - 「官网」指向作品页 `https://feiyu.ltd/zuopin/yuyin/`,「会员空间」打开 `https://feiyu.ltd/member/`。 - 默认快捷键对齐 QQ 音乐 PC 版:播放/暂停 `Ctrl+Alt+F5`、上一首 `Ctrl+Alt+Left`、 下一首 `Ctrl+Alt+Right`、音量 ± `Ctrl+Alt+Up/Down`;另保留 `F9` 显示/隐藏 115、 `Ctrl+F` 搜索、`Ctrl+Q` 退出。 - 设置 → 快捷键设置… 逐项录制组合键,保存前检查重复;绑定写 `meta.shortcuts`(JSON), 清空某项即禁用;"恢复默认"回填 QQ 音乐默认值。 ### 5.12 单曲详情(点击曲目) - 单击曲目表任意一首歌弹出非模态详情面板:歌名、所在位置、时长、大小、格式、缓存状态, 并提供"播放 / 缓存到本地(已缓存时置灰)/ 关闭"。 - 单击信号延迟到双击判定之后再发出,双击仍只播放、不弹详情;缓存状态随下载进度实时刷新。 ### 5.13 飞鱼会员与云备份(`MemberCloudService`) - 账号即 feiyu.ltd 站点账号:`POST /api/member/login/` 换取 Token,存 `meta.feiyu_token`; Token 失效时自动回落到登录页。 - 对话框一窗两态:未登录显示账号密码 + 注册入口(`/member/register/`);登录后显示作品下拉、 备份说明与云端备份列表,可上传当前配置、下载到本地、恢复、删除,并附「会员空间」「退出登录」。 - 备份包内容:`yuyin.db`(走 SQLite 在线备份接口写快照,保证一致性)+ 浏览器 Profile (跳过缓存,约 2 MB),打包 zip 上传;超过 20 MB 直接提示,不发起上传。 打包时遇到浏览器独占的 Profile 文件(如 Cookies)自动改走 SQLite 快照或共享读取, 仍失败的跳过并在对话框中提示,不中断整个备份。 - 恢复采用「先退出、下次启动覆盖」:点「恢复到本机」→ 下载 → 暂存 → 应用退出 → 下次启动时在 数据库与 Profile 打开之前应用,避免运行中写坏 SQLite / Profile;解压时拒绝绝对路径与 `..` 等非法条目。 - 入口:设置 → 飞鱼会员 / 云备份…;帮助 → 会员空间。 - 侧栏底部常驻会员状态块:登录后显示账号首字母头像、账号名与等级(VIP 为金色,并在副行显示到期日); 未登录显示灰色「?」与「点击登录飞鱼会员」,点击直接打开会员对话框。侧栏折叠后只保留头像, 仍能一眼看出登录状态,悬停可看完整等级与到期信息。 ### 5.14 在线更新(作品页单一来源) - 版本信息只认作品页 `https://feiyu.ltd/zuopin/yuyin/`:从版本徽标取版本号、从「下载安装包」 按钮取安装包地址、从「更新历史」取该版本的更新说明。 - 检查更新:拉取作品页并与本地 `APP_VERSION` 比较,有新版本时弹窗展示更新说明; 「跳过此版本」写 `meta.update_skip`,设置里手动检查时忽略跳过。 - 更新下载窗口:640×540,展示本次更新内容、下载进度与状态,右下提供「暂停 / 继续」「停止」 「关闭」;停止会中断传输并删除未完成的安装包,下载中直接关窗等同于点「停止」。 - 下载完成后应用立即退出并启动安装包静默安装;安装时会自动关闭仍在运行的程序实例, 避免文件占用导致安装失败。 - 版本号固定 `0.1.2`(开发期不随每次改动递增),发布方式是用同版本号重新打包并覆盖 `Yuyin-Setup-0.1.2.exe`。 --- ## 6. 代码结构(M0 落地) ``` Yuyin/ ├── .venv/ # 虚拟环境(已创建,依赖已装) ├── README.md # 本文档 ├── requirements.txt ├── Yuyin.py # 启动入口(自动切到 venv 运行 app.main) ├── main.ico # 应用图标(多分辨率,窗口/任务栏/托盘统一使用) ├── third_party/mpv/ # M4:mpv.exe + 许可证 └── app/ ├── main.py # 入口:QApplication、主题、MainWindow ├── config.py # 路径、UA、扩展名白名单、常量 ├── core/ │ ├── browser_kernel.py # 持久化 Profile + QWebEngineView │ ├── page_api.py # PageApiBridge:页面内 fetch + 轮询回传 │ ├── session.py # SessionService:登录校验与状态信号 │ ├── cloud115.py # Cloud115Client:目录/搜索/我听接口 │ ├── library.py # LibraryService:目录分页、音频过滤、搜索 │ ├── listen.py # ListenService:我听听单与曲目 │ ├── cookies.py # CookieStore:内存 Cookie 罐(含 path 匹配) │ ├── resolver.py # PlayUrlResolver:pickcode → CDN 直链(TTL 缓存) │ ├── proxy.py # LocalStreamProxy:127.0.0.1 流代理 │ ├── cache.py # CacheService:离线下载与 LRU(M5) │ ├── member_cloud.py # MemberCloudService:会员登录、作品列表、云备份上传/恢复 │ ├── updater.py # UpdateService:作品页版本解析、断点式下载(暂停/继续/停止) │ └── store.py # SQLite 访问层(已落地) ├── player/ │ ├── backend.py # PlaybackBackend 接口 + QtBackend │ ├── mpv_backend.py # MpvBackend(JSON IPC) │ ├── engine.py # PlayerService:队列/状态机/后端切换 │ └── tagreader.py # mutagen 标签/封面/歌词 └── ui/ ├── main_window.py # 整体布局:侧栏 + 内容 + 播放条 ├── sidebar.py # 左侧导航 ├── content_area.py # 顶栏 + 页面栈 + 搜索入口 ├── library_panel.py # 目录树 + 歌曲表(已落地) ├── listen_panel.py # 我听听单 + 曲目表(已落地) ├── track_table.py # 通用曲目表格 ├── cache_page.py # 缓存管理页(已落地) ├── shortcuts.py # 快捷键管理 + 设置对话框 ├── track_detail.py # 单曲详情面板 ├── member_dialog.py # 飞鱼会员 / 云备份对话框(登录、上传、恢复) ├── update_dialog.py # 更新下载窗口(更新内容 + 暂停/继续/停止) ├── browser_panel.py # 地址栏 + QWebEngineView ├── player_bar.py # 底部播放条 ├── queue_drawer.py # 播放队列抽屉(M6) └── style.qss # QQ 音乐风格浅色主题 ``` --- ## 7. 界面方案 - 布局参考 QQ 音乐桌面端:**左侧导航 + 中间内容 + 底部播放条 + 右侧可收起的播放队列**; 115 网页作为右侧抽屉/分栏滑出(F9 折叠),不打断播放。 - 主窗口 1280×820,最小 1024×680;侧栏 208px,播放条 84px。 - 配色:主色 `#31C27C`(QQ 音乐绿),背景 `#F6F7F9`,卡片白底 + 8px 圆角, 主文字 `#222222`、次文字 `#8A8F99`,分割线 `#EDEFF2`;默认浅色,深色主题放 M6。 - 侧栏:Logo + 发现音乐 / 我的音乐 / 我听 / 本地缓存 / 设置;选中项浅绿底 + 绿字 + 左侧指示条。 - 内容区:顶栏(页面标题、搜索框、登录状态、打开 115 按钮)+ 歌单卡片网格 + 歌曲列表。 - 播放条:左(封面 56×56、标题、歌手、缓存标记)、中(上一首 / 播放 / 下一首、进度、时间)、 右(播放模式、音量、队列)。 - 列表交互:悬停高亮、双击播放、右键菜单(播放 / 下一首播放 / 缓存 / 查看所在目录)。 - 空状态文案明确下一步动作(未登录 → "登录 115";无缓存 → "去音乐库挑几首")。 - 图标用内联 SVG/矢量绘制,不复制 QQ 音乐的图标、字体、图片素材,只借鉴布局与交互习惯。 - 快捷键:`Space`、`←/→`、`Ctrl+←/→`、`Ctrl+F`、`F9`(网页抽屉)、`Ctrl+Q`。 --- ## 8. 风险与对策 | 风险 | 影响 | 对策 | | --- | --- | --- | | 115 WebAPI 改版或风控 | 列表/直链失败 | 端点与参数集中在一个文件;失败时给出可读提示;保留 CDP 方案 B | | 115 WAF 拦截 Python 直连(实测 405) | requests 调 API 全挂 | 统一走页面内 `fetch`(5.2),Python 只负责编排与播放 | | Cookie 为 HttpOnly、取不到 | 无法调 API | 走 CookieStore 原生接口,不依赖 JS | | 直链短时效 / 绑定 UA、IP | 播到一半中断 | 本地代理统一补头;403 时自动换链并恢复进度 | | QtWebEngine Chromium 版本与 115 页面不兼容 | 无法登录/白屏 | 用最新 PyQt6;必要时切方案 B;提供"外部浏览器登录后导入 Cookie"的降级入口(可选) | | 「我听」无公开文档 | 听单读不到 | 先抓包确认;接口不可用时页面 JS 只读兜底;最后退化为仅网盘目录 | | 版权与合规 | 账号风险 | 只播自有文件、正常接口低频访问,不做导出下载与破解 | | 音频格式不全(ape/ogg/opus) | 部分歌不能播 | M4 双后端,mpv 兜底;列表标注解码后端 | | mpv 分发许可(GPLv2+) | 合规风险 | 附许可证与源码获取方式;MVP 允许用户自备 `mpv.exe` | | 离线缓存占满磁盘 | 磁盘告警 | 默认 10GB 上限 + LRU + 缓存管理页;下载前检查剩余空间 | | 界面模仿的版权边界 | 视觉争议 | 只借鉴布局与交互,不复制 QQ 音乐图标、字体、图片素材 | | 大目录卡顿 | 体验差 | 分页 100~500/页、懒加载、SQLite 索引、后台线程 | | 打包体积大(WebEngine + mpv) | 分发不便 | 先以源码 + venv 运行;后期 PyInstaller onedir | --- ## 9. 里程碑 > 状态:M0、M1、M2、M3、M4、M5 已完成并实测(登录态、目录树、搜索、我听、直链播放、 > mpv 双后端与自动兜底、离线缓存与缓存优先播放均正常);M6 待开始。 | 阶段 | 交付物 | 验收标准 | | --- | --- | --- | | **M0 环境与骨架** | venv、requirements.txt、Yuyin.py、QQ 音乐风格主窗口骨架(侧栏 / 内容区 / 播放条,占位) | `python Yuyin.py` 打开窗口无报错,布局与配色符合第 7 节 | | **M1 登录与连通** | 内嵌 115 页面、持久化登录、Cookie 桥、根目录拉取 | 网页登录后重启仍登录;能打印根目录条目;登录状态灯正确 | | **M2 音乐库 + 我听** | 目录树、音频过滤、搜索、SQLite 索引;我听抓包与听单列表 | 任意含音频目录可列出歌曲;我听页能列出听单并进入详情 | | **M3 播放闭环** | 直链解析、本地代理、Qt 后端、播放条联动、循环/切歌 | 双击可播、拖动进度、切歌无卡死;断链自动恢复一次 | | **M4 mpv 后端(提前)** | `third_party/mpv/`、MPV IPC 封装、后端自动选择、格式兜底 | ape/ogg/opus 可直接播放;Qt 失败能自动切 mpv 并续上进度 | | **M5 离线缓存** | 下载队列、断点续传、LRU 上限、缓存优先播放、缓存管理页 | 已缓存曲目断网可播;下载中断可续;容量超限自动清理 | | **M6 体验与打包** | 队列抽屉、歌词、快捷键、深浅主题、托盘/媒体键、PyInstaller | 关闭重开恢复队列与音量;打包后能启动并播放 | 每个里程碑结束做一次自查:主线程无阻塞、无 Cookie 明文落盘、异常可恢复。 --- ## 10. 运行约定 ```powershell # 启动(自动使用 .venv,无需先激活) python Yuyin.py ``` - 本地代理端口:`17890`(配置文件可改,被占用时自动 +1 重试)。 - 任务栏:启动时声明 `AppUserModelID=Yuyin.Desktop.App`,图标取根目录 `main.ico`, 直接复用 ico 内的多分辨率,避免任务栏沿用 pythonw 图标。 - 数据目录:程序目录下的 `Data\`(`yuyin.db`、`profile/`、`logs/`、`cache/`),绿色便携, 整个程序目录拷走即可迁移;音频缓存默认在程序目录的 `Cache\`。 旧版 FyMusic 放在 `%LOCALAPPDATA%\FyMusic\` 或旧安装目录 `Data\` 中的数据(歌单库、登录 Cookie) 会在首次启动时自动迁移到余音的数据目录。 - 日志:按天滚动,默认 INFO,可在设置里开 DEBUG;日志中 Cookie 一律脱敏。 - 配置:`config.json` 与 SQLite `meta` 表,二者冲突时以文件为准。 --- ## 11. 已确认决策(2026-10-28) 1. **内核**:采用 QtWebEngine 内嵌(方案 A);CDP 仅作兜底。 2. **范围**:除网盘目录外,同时接管 115「我听」听单。 3. **离线**:支持应用内缓存到本地,不做导出/另存。 4. **界面**:模仿 QQ 音乐布局与配色(主色 `#31C27C`),默认浅色。 5. **播放**:提前落地 mpv 后端(M4),与 Qt 后端并存。 6. **更新通道**(2026-10-31):版本检查与下载统一走 feiyu.ltd 作品页,不再使用 Gitee Release; 更新下载窗口展示本次更新说明,并支持暂停 / 继续 / 停止。 7. **版本与发布**(2026-10-31):版本号固定 `0.1.2`,日常改动不递增版本,发布即用同版本号 重新打包覆盖 `Yuyin-Setup-0.1.2.exe`,并同步作品页体积、下载链接与中英技术参数。 8. **会员与云备份**(2026-10-31):客户端接入 feiyu.ltd 会员账号,云备份采用「先退出、 下次启动覆盖」的方式恢复歌单库与网盘登录态。 9. **品牌更名**(2026-10-31):软件更名为「余音」,宣传语「余音不绝,绕梁三日」; 程序文件与安装目录使用英文标识 `Yuyin`,数据文件改为 `yuyin.db`,旧版 FyMusic 数据自动迁移; 作品页迁移到 `/zuopin/yuyin/`,旧地址跳转保留。 后续如需调整,直接改本节并同步第 9 节里程碑。 --- ## 附录 A:115 接口清单(待抓包校准) | 用途 | 方法与地址 | 关键参数/返回 | 状态 | | --- | --- | --- | --- | | 登录页 | `https://115.com/`(扫码登录卡片在首页内,`?ct=login` 已返回 404) | 内嵌网页完成 | 已确认 | | 文件列表 | `GET https://webapi.115.com/files`(页面内 fetch) | `aid/cid/offset/limit/show_dir/o/asc/count_folders`;`state/count/data[]`;文件带 `fid`,目录带 `cid/pid` | 已确认可用 | | 文件搜索 | `GET https://webapi.115.com/files/search` | `aid/cid=0/search_value/limit/offset`;`state/count/data[]`,文件带 `dp` 所在目录 | 已确认可用 | | 下载直链 | `GET https://webapi.115.com/files/download` | `pickcode`;`file_url` | 已验证(M3) | | 播放接口(备选) | `GET https://webapi.115.com/files/play` | `pickcode`;m3u8/mp4 | 待评估(M3) | | 我听列表 | `GET https://webapi.115.com/files/musicnew?topic_id=1&type=1&user_id=` | `data.list[]`:`topic_id/topic_name/music_count/pic/topic_sort`;含 `curr_user.user_id` | 已确认可用 | | 我听列表兜底 | `GET https://webapi.115.com/files/music_topic_listnew?fond=1&start=0&limit=20` | `data.data.list[]` | 已确认可用 | | 我听详情 | `GET https://webapi.115.com/files/music_list?topic_id=&start=0&limit=1150` | `data[]`:`id/file_name/pick_code/file_size/play_long/url` | 已确认可用 | | 我听播放(备选) | `GET https://webapi.115.com/files/music?pickcode=…&topic_id=…&music_id=…` | 条目自带的 `url` 字段 | 待验证(M3) | | 含音频目录 | `GET https://webapi.115.com/files/include_music_list` | `data[]`:`cid/pid/n` | 已确认可用(暂未使用) | | 用户信息 | `GET https://webapi.115.com/user/info` | `state:true` + 脱敏账号 | 已确认可用 | > 说明:以上均为 115 网页端自身调用的接口,仅用于播放当前账号自己的文件; > 字段与错误码在 M2 阶段已用内嵌页面实测核对,后续站点改版时同步更新此表。 ## 附录 B:术语 - **pickcode(pc)**:115 文件的提取码,取直链的核心标识。 - **cid**:115 目录 ID,根目录为 0。 - **Cookie 桥**:把浏览器内核里的登录 Cookie 同步给 Python 网络层的机制。 - **本地流代理**:把需要鉴权的远端地址包装成本机稳定 HTTP 地址的中间层。