Skip to content

fix(Unblock): 修复解锁音源搜索返回错误歌曲的问题#1008

Open
Mizoreww wants to merge 3 commits intoimsyy:devfrom
Mizoreww:fix/unblock-song-mismatch
Open

fix(Unblock): 修复解锁音源搜索返回错误歌曲的问题#1008
Mizoreww wants to merge 3 commits intoimsyy:devfrom
Mizoreww:fix/unblock-song-mismatch

Conversation

@Mizoreww
Copy link

@Mizoreww Mizoreww commented Mar 19, 2026

问题描述

搜索并播放"山雀"(万能青年旅店)时,实际播放的音频是"杀死那个石家庄人"——一首完全不同的歌。

根因分析

当网易云官方音源不可用时(无版权/VIP 限制),系统通过 Unblock 机制使用关键词(如 山雀-万能青年旅店)在第三方平台(酷我/波点/歌曲宝)搜索替代音源。

问题有三层:

  1. Bodian/Gequbao 直接取搜索结果第一条,不校验歌名,第三方搜索排序不可控导致播放错误歌曲
  2. 依赖 split("-") 解析歌名,带连字符的歌名(如 Anti-Hero)会被截断,导致误匹配
  3. 匹配逻辑大小写敏感且未去括号Hello (Live) vs Hello 会被误杀,合法匹配返回 404

此外,SongManager.tsfreeTrialInfo !== null 的严格不等判断存在隐患:当 VIP 用户获取到完整音频链接时,freeTrialInfoundefined(非 null),被误判为"试听",导致有效的官方 URL 被丢弃而走 Unblock。

修复内容

  1. 新增 match.ts:提取公共匹配逻辑
    • normalizeName():归一化歌名(小写 + 去除括号及其内容)
    • isSongMatch():同时校验歌名和艺术家,双向 includes
  2. 前端传递结构化参数unlockSongUrl 新增 songNameartist 参数,不再依赖 split("-") 解析
  3. 三个 unblock 源(Kuwo/Bodian/Gequbao):遍历全部搜索结果,用 isSongMatch 校验,无匹配则返回空
  4. SongManagerfreeTrialInfo !== nullfreeTrialInfo != null

测试

  • 搜索"山雀"(万能青年旅店)→ 修复前播放"杀死那个石家庄人",修复后正确拦截不匹配的搜索结果
  • 日志可见 ⚠️ Bodian 搜索结果均不匹配原曲: "山雀" 的警告
  • TypeScript 类型检查通过(pnpm typecheck:node + pnpm typecheck:web

Copilot AI review requested due to automatic review settings March 19, 2026 14:37
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求主要解决了在解锁音源搜索过程中可能返回错误歌曲的问题,以及 SongManager 中试听信息判断不准确的隐患。此前,当官方音源不可用时,系统通过第三方平台搜索替代音源时,由于未严格校验歌名,可能导致播放不匹配的歌曲。同时,SongManager 对 freeTrialInfo 的判断逻辑未能正确处理所有非 null 值,导致部分有效链接被误判。本次更新通过增强第三方音源的搜索匹配精确度,并修正 SongManager 的判断逻辑来提升播放体验。值得注意的是,补丁显示相关的音源文件和 SongManager 文件已被移除,这可能意味着这些功能已被重构或迁移到新的实现中。

Highlights

  • 第三方音源搜索优化: 改进了波点、酷我、歌曲宝等第三方音源的搜索匹配逻辑,现在会遍历搜索结果并选择与原曲名匹配的项,避免播放不正确的歌曲。
  • SongManager 试听判断修正: 将 SongManager 中 freeTrialInfo 的判断从 !== null 更改为 != null,以正确处理 undefined 值,确保 VIP 用户能正常获取完整音频链接。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

This comment was marked as outdated.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

本次 PR 解决了解锁音源可能返回错误歌曲的关键问题。通过修改 Bodian、酷我和歌曲宝音源的搜索逻辑,对搜索结果和原歌曲名进行校验,有效避免了歌曲不匹配的情况。此外,还修复了 SongManager.ts 中对试听歌曲的判断逻辑,通过使用非严格不等判断,正确处理了 nullundefined 的情况。这些改动提升了音乐播放功能的稳定性和正确性。

I am having trouble creating individual review comments. Click here to see my feedback.

electron/server/unblock/bodian.ts (81-82)

high

当前实现总是直接获取搜索结果列表中的第一项 (list[0]),这种做法并不可靠。因为第三方平台的搜索结果排序我们无法控制,可能会把一首热度更高但不正确的歌曲排在最前面。为了确保播放正确的歌曲,应该遍历整个 list 列表,找到第一个歌名与原始查询匹配的歌曲。如果找不到匹配项,最好返回 null,以避免播放错误的歌曲。

    const originalName = info.split("-")[0].trim();
    const song = list.find((item) => item.name.includes(originalName));
    return song?.id || null;

electron/server/unblock/gequbao.ts (18-23)

high

当前实现通过正则表达式匹配页面上的第一个音乐链接并返回其 ID。这种方式不够可靠,因为第一个结果不一定是正确的歌曲。为了提高准确性,应修改实现,改为获取页面上所有的歌曲搜索结果,提取它们的歌名和 ID,然后遍历这些结果,找到与关键词中原始歌名相匹配的一项。

electron/server/unblock/kuwo.ts (22-27)

high

此函数只处理了搜索结果中的第一项 (abslist[0])。虽然代码中包含了对歌名的校验,但如果正确的歌曲没有排在第一位,这个校验就失去了意义。逻辑应更新为遍历整个 abslist 列表,以查找与原始歌名匹配的歌曲。这样可以确保即使热门结果不正确,函数仍然能从列表的其余部分找到并返回正确的歌曲 ID。

    const songList = result.data.content[1].musicpage.abslist;
    const originalSongName = keyword.split("-")[0].trim();
    
    const foundSong = songList.find((song: any) => song.SONGNAME?.includes(originalSongName));

    if (foundSong && foundSong.MUSICRID) {
      return foundSong.MUSICRID.slice("MUSIC_".length);
    }

    return null;

src/core/player/SongManager.ts (225)

high

此处使用严格不等于 !== null 来检查 freeTrialInfo 会导致逻辑问题。当 VIP 用户获取到完整音频链接时,API 返回的 freeTrialInfo 可能是 undefined,此时 undefined !== null 的结果为 true,会错误地将歌曲判断为“试听”状态。改用非严格不等于 != null 可以同时判断 nullundefined,从而确保能正确处理具有完整播放权限的歌曲。

    const isTrial = songData?.freeTrialInfo != null;

@MoYingJi
Copy link
Collaborator

What are you doing (?
我建议你再看一下你的更改(

@MoYingJi MoYingJi marked this pull request as draft March 19, 2026 14:41
@Mizoreww Mizoreww force-pushed the fix/unblock-song-mismatch branch from d6a99fb to c3afc44 Compare March 19, 2026 14:48
@MoYingJi MoYingJi marked this pull request as ready for review March 19, 2026 15:11
@MoYingJi MoYingJi requested a review from Copilot March 19, 2026 15:20

This comment was marked as outdated.

@Mizoreww Mizoreww force-pushed the fix/unblock-song-mismatch branch from c3afc44 to a11d429 Compare March 19, 2026 15:29
@Mizoreww
Copy link
Author

抱歉,上一次推送的文件由于通过 API 上传时 blob 处理异常,导致 diff 显示为文件删除而非修改,已在最新提交中修正。

@Mizoreww
Copy link
Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

本次 PR 修复了解锁音源时因匹配逻辑不严谨而返回错误歌曲的问题,做得非常出色。通过引入结构化的歌曲信息(歌名、艺术家)和更鲁棒的匹配函数 isSongMatch,从根本上解决了问题。代码重构清晰,将通用逻辑提取到 match.ts 中,并兼容了旧的 API 调用方式。此外,还修复了 SongManager 中对试听状态的错误判断。整体而言,这是一次高质量的修复。我只提出一个关于处理多艺术家歌曲的小建议,以进一步提高匹配的准确率。

}
const artist = Array.isArray(song.artists) ? song.artists[0].name : song.artists;
const keyWord = song.name + "-" + artist;
const artistName = Array.isArray(song.artists) ? song.artists[0].name : song.artists;
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

这里的逻辑只取了第一个艺术家,对于多位艺术家的歌曲,这可能会导致解锁失败。例如,如果歌曲有 A 和 B 两位艺术家,这里只取了 A,但第三方源可能将 B 列为主要艺术家,或显示为“A & B”,导致匹配失败。建议将所有艺术家的名字连接起来,以提高匹配的准确性。

Suggested change
const artistName = Array.isArray(song.artists) ? song.artists[0].name : song.artists;
const artistName = Array.isArray(song.artists) ? song.artists.map((a) => a.name).join(" & ") : song.artists;

- 新增 match.ts 提供归一化匹配(歌名去括号 + 艺术家统一分隔符)
- 前端传递结构化参数 songName/artist,不再依赖 split("-") 解析
- 三个 unblock 源遍历全部搜索结果,同时校验歌名和艺术家
- 修复 DownloadManager 也传递 songName/artist(与播放链路一致)
- Kuwo 搜索 URL 对 keyword 做 encodeURIComponent 防止 & 截断
- 多艺术家合作歌曲传递所有艺术家名(join " & ")
- 修复 freeTrialInfo !== null → != null 避免 VIP 链接被误丢弃
- 更新 JSDoc 参数说明
@Mizoreww Mizoreww force-pushed the fix/unblock-song-mismatch branch from a11d429 to ebea684 Compare March 19, 2026 16:28
@Mizoreww
Copy link
Author

更新了

@Mizoreww
Copy link
Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

您好,这次的 PR 很好地解决了音源解锁时匹配错误歌曲的问题。通过重构匹配逻辑、传递结构化数据(歌名、艺术家)以及在服务端进行更严格的校验,从根本上提升了准确性。代码结构清晰,特别是新增的 match.ts 模块,将通用逻辑提取出来,非常棒。对 SongManager.tsfreeTrialInfo 的判断修复也很到位。我只有一个关于旧关键字解析回退逻辑的小建议,详见具体评论。

Copy link
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 修复在官方音源不可用时(走 Unblock 第三方搜索替代)可能返回并播放错误歌曲的问题,通过引入“结构化原曲信息 + 结果遍历匹配”来提升命中准确性,并修正 VIP 场景下被误判为“试听”导致错误走 Unblock 的问题。

Changes:

  • 新增服务端公共匹配模块 match.ts,对歌名/艺人做归一化并进行匹配校验
  • 前端调用 unlockSongUrl 时传递 songName / artist,服务端构造 SongMatchInfo 并用于 Kuwo/Bodian/Gequbao 的结果遍历匹配
  • 修正 SongManagerfreeTrialInfo 的判定逻辑,避免误判丢弃官方 URL

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/core/resource/DownloadManager.ts Unblock 下载请求补充 songName/artist,并调整艺术家拼接格式用于关键词
src/core/player/SongManager.ts 修正试听判定;Unblock 请求补充 songName/artist;关键词构造更完整
src/api/song.ts 扩展 unlockSongUrl 参数并把 songName/artist 传到 /api/unblock
electron/server/unblock/unblock.d.ts 新增 SongMatchInfo 类型用于服务端匹配上下文
electron/server/unblock/match.ts 新增归一化与匹配函数,供各 Unblock 源复用
electron/server/unblock/index.ts 构建匹配信息(兼容旧 keyword 解析),并传递给各源实现
electron/server/unblock/kuwo.ts 遍历搜索结果并用 isSongMatch 校验,避免取第一条导致错歌
electron/server/unblock/bodian.ts 遍历搜索结果并用 isSongMatch 校验,避免取第一条导致错歌
electron/server/unblock/gequbao.ts 解析搜索页多个结果并用 isSongMatch 校验,避免误命中

Comment on lines +35 to +41
const normalizedResult = normalizeName(resultName);
const normalizedOriginal = normalizeName(match.songName);
// 歌名:双向 includes(兼容一方带后缀的情况)
if (
!normalizedResult.includes(normalizedOriginal) &&
!normalizedOriginal.includes(normalizedResult)
) {
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

isSongMatch currently treats empty/blank song names as a match because String.prototype.includes("") is always true. If resultName (or the normalized form after removing brackets) ends up empty, the name check will incorrectly pass and the first search result could be accepted. Add an explicit guard so an empty normalized result/original name returns false before doing the bidirectional includes check.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +52
if (resultArtist && match.artist) {
const normalizedResultArtist = normalizeArtist(resultArtist);
const normalizedOriginalArtist = normalizeArtist(match.artist);
if (
!normalizedResultArtist.includes(normalizedOriginalArtist) &&
!normalizedOriginalArtist.includes(normalizedResultArtist)
) {
return false;
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

The artist check has the same empty-string pitfall after normalization (e.g. resultArtist is whitespace-only): includes("") will pass and the artist constraint becomes ineffective. Consider normalizing first and requiring both normalized artist strings to be non-empty before applying the bidirectional includes logic; otherwise return false when an artist string is provided but normalizes to empty.

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +57
// 构造匹配信息(fallback 用 lastIndexOf 兼容歌名含连字符的情况)
const buildMatchInfo = (query: { [key: string]: string }) => {
let songName = query.songName || "";
let artist = query.artist || "";
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

SongUrlResult is only used as a TypeScript type in this file, but it’s imported as a value (import { SongUrlResult } ...). Since ./unblock is a .d.ts-only module, preserving value imports (e.g. with verbatimModuleSyntax / future TS config changes) could turn this into a runtime import that fails. Switch this to import type to make the runtime dependency impossible.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

@MoYingJi MoYingJi left a comment

Choose a reason for hiding this comment

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

代码看着没有大问题,但我的账号有会员没法进行测试(

Copy link
Collaborator

@MoYingJi MoYingJi left a comment

Choose a reason for hiding this comment

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

差点忘了还有个 API 文档,在 /docs/api.md

SPlayer/docs/api.md

Lines 253 to 312 in 405e01d

### 酷我解锁
**接口**: `GET /api/unblock/kuwo?keyword={keyword}`
**描述**: 获取酷我音乐解锁后的播放链接
**请求参数**:
- `keyword` (必需): 搜索关键词(歌曲名或歌手名)
**响应示例**:
```json
{
"code": 200,
"url": "https://..."
}
```
---
### 波点解锁
**接口**: `GET /api/unblock/bodian?keyword={keyword}`
**描述**: 获取波点音乐解锁后的播放链接
**请求参数**:
- `keyword` (必需): 搜索关键词(歌曲名或歌手名)
**响应示例**:
```json
{
"code": 200,
"url": "https://..."
}
```
---
### 歌曲宝解锁
**接口**: `GET /api/unblock/gequbao?keyword={keyword}`
**描述**: 获取歌曲宝解锁后的播放链接
**请求参数**:
- `keyword` (必需): 搜索关键词(歌曲名或歌手名)
**响应示例**:
```json
{
"code": 200,
"url": "https://..."
}
```

此外,为了保持 API 的兼容性,songNameartist 都需要为可选参数(不填保持原有匹配逻辑,现在应该是这样的,就像 AI 所说的 includes 空字符串,不过逻辑不是显式的)

Comment on lines +44 to +54
// 艺术家:归一化分隔符后双向 includes
if (resultArtist && match.artist) {
const normalizedResultArtist = normalizeArtist(resultArtist);
const normalizedOriginalArtist = normalizeArtist(match.artist);
if (
!normalizedResultArtist.includes(normalizedOriginalArtist) &&
!normalizedOriginalArtist.includes(normalizedResultArtist)
) {
return false;
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

这样貌似也没法解决 AI 所说的 A & BB & A 的比较问题(a bb a 互不包含),只能解决像 AA & B 这样的

我不知道这样的检查是否过于严格,我也没有账号测试这是否影响实际其它歌曲的匹配体验

- isSongMatch 增加空字符串保护,避免 includes("") 恒为 true
- SongUrlResult 改为 import type
- 补充 songName/artist 可选参数文档
@Mizoreww
Copy link
Author

已修复 review 中提到的问题:

  1. isSongMatch 增加空字符串保护,避免 includes("") 恒为 true
  2. SongUrlResult 改为 import type
  3. 补充了 API 文档中 songName/artist 可选参数说明

本地已测试通过,感谢 review!

Comment on lines +37 to +38
// 空字符串保护:避免 includes("") 恒为 true
if (!normalizedResult || !normalizedOriginal) return false;
Copy link
Collaborator

Choose a reason for hiding this comment

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

这样 songName 是否就不是可选参数了?(songName 为空时这里直接返回 false 使得没有任何歌曲能够匹配,预期行为是保持旧行为,即直接忽略歌曲的歌名)

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.

3 participants