Skip to content

feat: 完善在线播放#19

Merged
imsyy merged 33 commits into
devfrom
online
May 20, 2026
Merged

feat: 完善在线播放#19
imsyy merged 33 commits into
devfrom
online

Conversation

@imsyy
Copy link
Copy Markdown
Member

@imsyy imsyy commented May 15, 2026

No description provided.

@imsyy imsyy marked this pull request as ready for review May 17, 2026 16:35
Copilot AI review requested due to automatic review settings May 17, 2026 16:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

该 PR 主要围绕“在线播放”能力补全:引入在线平台(网易云/QQ 音乐/酷狗)搜索与曲目建模、在线音频 URL 解析(官方接口 + 插件兜底)、在线歌词拉取与 TTML 升级策略,并新增网易云登录与用户侧入口;同时加入一个可选的外部控制 API(HTTP + WebSocket)以及若干 UI/组件能力调整来支撑上述功能在界面端落地。

Changes:

  • 新增在线平台的数据类型/转换工具、搜索 API 与“合集/歌手页”按来源加载服务,打通在线内容浏览链路。
  • 在线播放地址解析接入(网易云官方 song_url + 插件解析兜底),并扩展错误码与缓存防护(拒绝 HTML/JSON 冒充音频)。
  • 新增网易云登录(二维码/网页登录/手动 Cookie)与外部 API 服务(REST + WS),并调整若干 UI 组件/导航入口。

Reviewed changes

Copilot reviewed 109 out of 115 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
uno.config.ts 新增 dialog 顶部弹出动画 keyframes/duration/easing
src/utils/time.ts 时间格式化支持 h:mm:ss
src/utils/netease.ts 网易云 raw 数据到应用层模型的转换工具
src/utils/navigate.ts 导航工具增强(非本地源 ID 约束 + 新增歌单跳转)
src/utils/format.ts 新增数字紧凑格式化(Intl compact)
src/utils/errors.ts 扩展可跳过错误码集合
src/types/user.ts 新增用户资料/订阅计数类型
src/types/netease.ts 新增网易云接口原始类型定义
src/stores/status.ts 新增搜索平台/我的歌单来源状态并持久化
src/stores/data.ts 新增通用本地数据 store(搜索历史)
src/settings/categories/services.ts 新增“外部 API(Beta)”设置区块与面板
src/services/lyricLoader.ts 在线歌词偏好策略增强 + TTML overlay 多平台候选
src/services/collectionLoader.ts 新增合集加载服务(local/streaming/netease)
src/services/audioSource.ts 在线音源 URL 解析接入(官方+插件)与缓存 key 扩展
src/services/artistLoader.ts 新增歌手页加载服务(local/streaming/netease)
src/router/index.ts 新增 /search 路由
src/pages/Streaming/Playlists.vue 歌单点击改用统一 navigateToPlaylist
src/pages/Folders.vue selectedFolder 改 shallowRef + toRaw 避免深层代理影响
src/pages/Collection.vue 合集页改走 collectionLoader + 增量更新/Abort/Loading 态
src/pages/Artist.vue 歌手页改走 artistLoader + 网易云歌曲触底分页 + Loading 态
src/layouts/components/SideBarLogo.vue 删除旧布局目录下的 SideBarLogo(迁移)
src/i18n/locales/zh-CN.json 新增搜索/登录/外部 API 文案与错误提示更新
src/i18n/locales/en-US.json 新增搜索/登录/外部 API 文案与错误提示更新
src/components/ui/SVirtualList.vue paddingBottom 处理调整(底部 spacer)+ footer 结构调整
src/components/ui/SInput.vue 支持 textarea/rows/resize + 清空按钮交互优化
src/components/ui/SDialog.vue 支持 top 偏移与 contentStyle,并新增顶部弹出动画分支
src/components/ui/SAlert.vue 新增通用 Alert 组件
src/components/settings/custom/LyricSourceOrderConfig.vue 平台显示名改用 PLATFORM_SHORT_NAME
src/components/settings/custom/ExternalApiPanel.vue 外部 API 状态展示与重启按钮面板
src/components/player/TrackInfo.vue 播放栏歌手支持按来源可跳转判断
src/components/player/FullPlayer/PlayerData.vue 全屏页歌手/专辑跳转约束 + sourceLabel 改进
src/components/modals/LoginDialog.vue 新增网易云登录弹窗(二维码+自动获取+手动入口)
src/components/modals/LoginCookieDialog.vue 新增手动 Cookie 登录弹窗
src/components/list/SongList.vue 列表跳转约束、付费标识、触底加载 footer 态支持
src/components/list/PlaylistPanel.vue 播放列表点击播放改用 player.playAtIndex
src/components/list/CoverList.vue 支持触底加载与 footer 状态(loading/noMore)
src/components/layout/WindowControls.vue 新增窗口控制按钮组件
src/components/layout/SideBarLogo.vue SideBar Logo 迁移到新目录并改点击回首页
src/components/layout/SideBar.vue 侧栏“我的歌单”支持本地/在线切换与订阅歌单分组
src/components/layout/NavUser.vue 新增用户入口(登录态/统计/退出)
src/components/layout/NavHeader.vue 头部加入搜索与用户入口
src/apis/user/netease.ts 新增网易云用户数据拉取(歌单/收藏/专辑/歌手/喜欢等)
src/apis/song/netease.ts 新增 song_detail 批量取歌 + song_url 播放地址解析
src/apis/search/suggest.ts 新增网易云搜索建议
src/apis/search/qqmusic.ts 新增 QQ 音乐搜索(歌曲)渲染端适配
src/apis/search/netease.ts 新增网易云云搜(歌曲/专辑/歌手/歌单)
src/apis/search/kugou.ts 新增酷狗搜索(歌曲)渲染端适配
src/apis/search/index.ts 新增跨平台搜索分发入口
src/apis/search/hot.ts 新增网易云热搜(TTL 缓存)
src/apis/netease.ts 渲染端 Netease Proxy 调整(泛型/注释)
src/apis/login/netease.ts 新增网易云登录相关 API(qrKey/qrCheck/status/refresh/logout)
shared/types/settings.ts 新增 ExternalApiSettings/Status 并挂到 SystemConfig
shared/types/plugin.ts SandboxOut 新增 sourcesUpdate 消息
shared/types/player.ts TrackSource 改为 Platform 并扩展 TrackFee/extId 等
shared/types/platform.ts 新增 PLATFORM_SHORT_NAME/ALL_PLATFORMS/isPlatform
shared/types/errors.ts 新增 URL_RESOLVE_FAILED/NO_PLUGIN_AVAILABLE 错误码
shared/types/apis.ts ApisApi 增加 openLoginWeb/setCookie
shared/defaults/settings.ts 增加 externalApi 默认配置
package.json 引入 hono/@hono/node-server/ws/uqr 及 @types/ws
native/audio-engine/src/player.rs stop() 清理 current_source,避免 Stopped 复活上一首
electron/preload/index.ts 新增 apis.openLoginWeb/apis.setCookie 与 externalApi IPC
electron/preload/index.d.ts window.api.externalApi 类型声明补全
electron/main/window/login.ts 新增网易云网页登录窗口(独立 session + 轮询 cookie)
electron/main/utils/logger.ts 新增 serverLog/pluginLog scope
electron/main/services/songCache.ts 缓存下载增加 MIME/文件头拦截,防错误页入缓存
electron/main/services/nowPlaying.ts 注释/参数说明补全(功能未改)
electron/main/server/ws.ts 新增 WS 指令分发与 ack/error 协议
electron/main/server/routes.ts 新增外部 API REST 路由(status/now-playing/controls)
electron/main/server/index.ts 新增外部 API server 启停与状态管理
electron/main/server/gate.ts 新增 externalApi.enabled/wsEnabled 门禁中间件
electron/main/server/broadcast.ts 新增 WS 客户端注册与事件广播(高频过滤)
electron/main/plugins/sandbox.worker.ts IPC 数据 sanitize + sourcesUpdate + btoa/atob 注入
electron/main/plugins/sandbox.ts Sandbox 事件新增 onSourcesUpdate
electron/main/plugins/router.ts resolveUrl 调用路径调整并增强日志
electron/main/plugins/registry.ts 合并异步 sourcesUpdate 到 runtime status
electron/main/plugins/lx-shim.ts lx.request headers/body 归一 + musicUrl 回包形状校验
electron/main/ipc/player.ts player 事件同时 wsBroadcast + 远端封面拉取逻辑泛化
electron/main/ipc/nowPlaying.ts nowPlaying 事件同时广播到外部 WS
electron/main/ipc/lyrics.ts 在线 Track 的 id/extId 处理调整 + 注释增强
electron/main/ipc/index.ts 注册 externalApi IPC
electron/main/ipc/externalApi.ts 新增 externalApi:restart/getStatus IPC
electron/main/ipc/apis.ts 新增网页登录与手动 cookie 写入 IPC
electron/main/core/index.ts 应用启动/退出时启动/停止外部 API 服务
electron/main/apis/qqmusic/modules/search.ts QM 搜索端点调整并限制仅歌曲搜索
electron/main/apis/netease/modules/song_url.ts 新增网易云 song_url(v1) 模块
electron/main/apis/netease/modules/song_detail.ts 新增网易云 song_detail 模块
electron/main/apis/netease/modules/playlist_detail.ts 新增网易云 playlist_detail 模块
electron/main/apis/netease/modules/likelist.ts 新增喜欢列表模块
electron/main/apis/netease/modules/like.ts 新增 like/取消 like 模块
electron/main/apis/netease/modules/index.ts 汇总新增模块导出
electron/main/apis/netease/modules/artists.ts 新增歌手信息模块
electron/main/apis/netease/modules/artist_sublist.ts 新增收藏歌手列表模块
electron/main/apis/netease/modules/artist_songs.ts 新增歌手歌曲分页模块
electron/main/apis/netease/modules/artist_album.ts 新增歌手专辑模块
electron/main/apis/netease/modules/album.ts 新增专辑详情模块
electron/main/apis/netease/modules/album_sublist.ts 新增收藏专辑列表模块
electron/main/apis/netease/core/request.ts 失败抛 NeteaseRequestError(保留 response)
electron/main/apis/kugou/index.ts 搜索缓存:空结果不缓存
electron/main/apis/kugou/core/request.ts KG 请求注释与成功码说明更新
electron/main/apis/kugou/core/config.ts 新增 mobilecdn 主搜索端点常量
electron/main/apis/common/lyric/utils.ts 候选匹配增加“时长差距过大”硬性过滤
electron/main/apis/common/lyric/qqmusic.ts QQMusic 歌词接口注释澄清 songID/mid
electron.vite.config.ts 组件自动导入目录调整(移除旧 layouts/components)
docs/plugins-usage.md lx 垫片能力说明更新
docs/plugins-development.md 插件全局能力/level1 约束/错误码说明更新
components.d.ts 自动组件声明更新(含新增组件/路径变更)
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)

src/services/audioSource.ts:166

  • 同上:在线源解析失败时 catch 分支把 err.message 当作错误码传入 handleError(),会导致只提示 UNKNOWN。建议改为 handleError(ErrorCode.URL_RESOLVE_FAILED/NETWORK_ERROR 等),并额外记录 err 细节到日志。

Comment on lines +77 to +78
// 启动外部 API 服务(fire-and-forget;监听结果通过 getStatus 暴露给渲染端)
void startServer();
Comment on lines +35 to +47
const port = store.get("externalApi.port");
const hostname = "0.0.0.0";

const app = new Hono();
app.use("/api/*", externalControlGate);
app.route("/api", buildRoutes());
app.get(
"/ws",
externalControlGate,
wsGate,
upgradeWebSocket(() => wsHandlers),
);
app.get("/", (c) => c.text("SPlayer Next external API"));
Comment on lines +61 to +65
key: "externalApiWs",
type: "switch",
binding: { store: "settings", path: "system.externalApi.wsEnabled" },
defaultValue: true,
},
Comment on lines 147 to 150
} catch (err) {
handleError(err instanceof Error ? err.message : String(err));
return null;
}
Comment on lines +14 to +18
<div class="flex items-center justify-center h-16 shrink-0 px-4">
<div
class="inline-flex items-center cursor-pointer transition-transform duration-300 hover:scale-105 active:scale-100"
@click="goHome"
>
Comment on lines +88 to +96
webPreferences: {
session: ses,
// sandbox 模式下 NCM 的 JS 渲染极慢;登录窗口里没有自家代码,关闭沙箱影响可控
sandbox: false,
spellcheck: false,
backgroundThrottling: false,
nodeIntegration: false,
contextIsolation: true,
},
Copilot AI review requested due to automatic review settings May 18, 2026 10:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 133 out of 139 changed files in this pull request and generated 10 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)

src/services/audioSource.ts:154

  • 这里把 async () => { ... } 赋给 cacheRequest(且调度器/调用方不会 await),容易出现未处理的 Promise 与类型不匹配问题。建议用同步包装(内部 void (async () => { ... })())或让调度器支持/await Promise。

Comment on lines +120 to +124
export interface ResolvedTrackSource {
source: string;
fromCache: boolean;
} | null> => {
cacheRequest?: () => void;
}
const cacheUrl = await store.getStreamUrl(track, {
playSessionId: crypto.randomUUID(),
});
void window.api.cache.song.fetch(cacheKey, "streaming", cacheUrl);
Comment on lines +77 to +78
// 启动外部 API 服务(fire-and-forget;监听结果通过 getStatus 暴露给渲染端)
void startServer();
Comment on lines +35 to +37
const port = store.get("externalApi.port");
const hostname = "0.0.0.0";

Comment thread src/utils/navigate.ts Outdated
Comment on lines +10 to +12
* @param albumName - 专辑名称(本地用作聚合 key;非本地用于 query 兜底显示)
* @param options.source - 来源(local/streaming/online);默认为 local
* @param options.albumId - 真实专辑 ID(streaming/online 必填)
Comment on lines +126 to +131
/** 订阅计数(/user/subcount)→ 应用层 UserSubcount */
export const toSubcount = (raw: any): UserSubcount => ({
createdPlaylistCount: raw.createdPlaylistCount ?? 0,
subPlaylistCount: raw.subPlaylistCount ?? 0,
artistCount: raw.artistCount ?? 0,
});
Comment on lines +14 to +18
<div class="flex items-center justify-center h-16 shrink-0 px-4">
<div
class="inline-flex items-center cursor-pointer transition-transform duration-300 hover:scale-105 active:scale-100"
@click="goHome"
>
Comment on lines +61 to +65
key: "externalApiWs",
type: "switch",
binding: { store: "settings", path: "system.externalApi.wsEnabled" },
defaultValue: true,
},
Comment thread src/stores/history.ts
Comment on lines +45 to +48
upgradeWebSocket(() => wsHandlers),
);
app.get("/", (c) => c.text("SPlayer Next external API"));

Copilot AI review requested due to automatic review settings May 18, 2026 17:30
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 137 out of 143 changed files in this pull request and generated 6 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +35 to +47
const port = store.get("externalApi.port");
const hostname = "0.0.0.0";

const app = new Hono();
app.use("/api/*", externalControlGate);
app.route("/api", buildRoutes());
app.get(
"/ws",
externalControlGate,
wsGate,
upgradeWebSocket(() => wsHandlers),
);
app.get("/", (c) => c.text("SPlayer Next external API"));
Comment on lines +77 to +78
// 启动外部 API 服务(fire-and-forget;监听结果通过 getStatus 暴露给渲染端)
void startServer();
Comment thread src/services/audioSource.ts Outdated
Comment on lines +49 to +58
const candidates = plugins.list.filter(
(info) =>
info.enabled &&
info.status.state === "ready" &&
info.status.sources[pluginSource]?.actions.includes("musicUrl"),
);
if (candidates.length === 0) {
handleError(ErrorCode.NO_PLUGIN_AVAILABLE);
return null;
}
Comment thread src/stores/history.ts Outdated
Comment on lines +24 to +49
const readFromDisk = async (): Promise<HistoryEntry[]> => {
const cached = await db.getItem<HistoryEntry[]>(HISTORY_KEY).catch(() => null);
return Array.isArray(cached) ? cached : [];
};

const apply = async (next: HistoryEntry[]): Promise<void> => {
entries.value = next;
await db.setItem(HISTORY_KEY, toRaw(next)).catch(() => {});
};

const load = async (): Promise<void> => {
entries.value = await readFromDisk();
};

/**
* 记录一次播放:每次都以硬盘为真值源,避免内存未 load 时覆盖旧历史
* @param track 当前播放曲目
*/
const record = async (track: Track): Promise<void> => {
if (!track?.id) return;
const key = keyOf(track);
const list = await readFromDisk();
const filtered = list.filter((item) => keyOf(item.track) !== key);
const next = [{ track, playedAt: Date.now() }, ...filtered].slice(0, MAX_HISTORY);
await apply(next);
};
Comment on lines +34 to +44
export const wsBroadcast = (event: { type: string; data?: unknown }): void => {
if (wsClients.size === 0) return;
if (HIGH_FREQ_EVENTS.has(event.type)) return;
const payload = JSON.stringify({ kind: "event", ...event });
for (const ws of wsClients) {
try {
ws.send(payload);
} catch {
// 单个客户端发送失败不影响其他人
}
}
Comment thread src/utils/format/netease.ts
Copilot AI review requested due to automatic review settings May 19, 2026 16:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 156 out of 162 changed files in this pull request and generated 5 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +35 to +40
const port = store.get("externalApi.port");
const hostname = "0.0.0.0";

const app = new Hono();
app.use("/api/*", externalControlGate);
app.route("/api", buildRoutes());
Comment on lines +6 to +12
interface Pending {
trackId: string;
fire: () => void;
}

let pending: Pending | null = null;

Comment thread src/stores/playlist.ts
Comment on lines 43 to 47
return {
...meta,
cover: tracks.find((t) => t.cover)?.cover,
tracks,
trackCount: tracks.length,
};
Comment on lines +15 to +20
<div
role="link"
tabindex="0"
class="inline-flex items-center cursor-pointer transform-gpu transition-transform duration-300 hover:scale-105 active:scale-100"
@click="goHome"
>
Comment on lines 96 to 100
};
const fingerprint = buildFingerprint(track);
const cached = getMatchedId(fingerprint, platform);
// QM mid 放前面
// QM mid 放前面(AMLL DB 早期 QM 条目以 mid 为文件名的居多)
if (platform === "qqmusic") push(cached?.extra?.mid);
Copilot AI review requested due to automatic review settings May 20, 2026 10:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 169 out of 175 changed files in this pull request and generated 8 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +35 to +37
const port = store.get("externalApi.port");
const hostname = "0.0.0.0";

Comment on lines +77 to 79
// 启动外部 API 服务(fire-and-forget;监听结果通过 getStatus 暴露给渲染端)
void startServer();
app.on("activate", () => {
Comment thread electron/main/server/routes.ts Outdated
Comment on lines +38 to +48
// 当前播放完整快照
api.get("/now-playing", (c) => {
const snap = nowPlaying.snapshot();
return c.json({
track: snap.track,
lyric: snap.lyric,
source: snap.source,
position: snap.position,
playing: snap.playing,
lyricOffsetMs: snap.lyricOffsetMs,
});
Comment on lines +10 to +16
/** 总开关 */
export const externalControlGate: MiddlewareHandler = async (c, next) => {
if (!store.get("externalApi.enabled")) {
return c.json({ error: "external API disabled" }, 403);
}
await next();
return;
Comment on lines +88 to +96
webPreferences: {
session: ses,
// sandbox 模式下 NCM 的 JS 渲染极慢;登录窗口里没有自家代码,关闭沙箱影响可控
sandbox: false,
spellcheck: false,
backgroundThrottling: false,
nodeIntegration: false,
contextIsolation: true,
},
Comment on lines +15 to +20
<div
role="link"
tabindex="0"
class="inline-flex items-center cursor-pointer transform-gpu transition-transform duration-300 hover:scale-105 active:scale-100"
@click="goHome"
>
Comment thread src/stores/media.ts
Comment on lines 54 to 57
const setTrack = (newTrack: Track, newDetail?: TrackDetail): void => {
track.value = newTrack;
detail.value = newDetail ?? null;
if (newDetail) detail.value = newDetail;
};
Comment thread src/components/list/CoverList.vue
Copilot AI review requested due to automatic review settings May 20, 2026 16:02
@imsyy imsyy merged commit 8990cec into dev May 20, 2026
5 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 172 out of 179 changed files in this pull request and generated 5 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment thread src/stores/media.ts
Comment on lines 54 to 57
const setTrack = (newTrack: Track, newDetail?: TrackDetail): void => {
track.value = newTrack;
detail.value = newDetail ?? null;
if (newDetail) detail.value = newDetail;
};
Comment on lines +77 to +78
// 启动外部 API 服务(fire-and-forget;监听结果通过 getStatus 暴露给渲染端)
void startServer();
Comment on lines +35 to +47
const port = store.get("externalApi.port");
const hostname = "0.0.0.0";

const app = new Hono();
app.use("/api/*", externalControlGate);
app.route("/api", buildRoutes());
app.get(
"/ws",
externalControlGate,
wsGate,
upgradeWebSocket(() => wsHandlers),
);
app.get("/", (c) => c.text("SPlayer Next external API"));
Comment on lines +36 to +40
api.get("/volume", (c) => c.json({ volume: getPlayer().getVolume() }));

// 当前播放完整快照
api.get("/now-playing", (c) => c.json(nowPlaying.snapshot()));

Comment on lines +15 to +20
<div
role="link"
tabindex="0"
class="inline-flex items-center cursor-pointer transform-gpu transition-transform duration-300 hover:scale-105 active:scale-100"
@click="goHome"
>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants