Skip to content

Commit 3849161

Browse files
authored
Merge pull request #1008 from Mizoreww/fix/unblock-song-mismatch
fix(Unblock): 修复解锁音源搜索返回错误歌曲的问题
2 parents 405e01d + d0bb11f commit 3849161

10 files changed

Lines changed: 191 additions & 71 deletions

File tree

docs/api.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,9 @@
258258

259259
**请求参数**:
260260

261-
- `keyword` (必需): 搜索关键词(歌曲名或歌手名)
261+
- `keyword` (必需): 搜索关键词(歌曲名-歌手名)
262+
- `songName` (可选): 歌曲名称,用于匹配校验
263+
- `artist` (可选): 歌手名称,用于匹配校验
262264

263265
**响应示例**:
264266

@@ -279,7 +281,9 @@
279281

280282
**请求参数**:
281283

282-
- `keyword` (必需): 搜索关键词(歌曲名或歌手名)
284+
- `keyword` (必需): 搜索关键词(歌曲名-歌手名)
285+
- `songName` (可选): 歌曲名称,用于匹配校验
286+
- `artist` (可选): 歌手名称,用于匹配校验
283287

284288
**响应示例**:
285289

@@ -300,7 +304,9 @@
300304

301305
**请求参数**:
302306

303-
- `keyword` (必需): 搜索关键词(歌曲名或歌手名)
307+
- `keyword` (必需): 搜索关键词(歌曲名-歌手名)
308+
- `songName` (可选): 歌曲名称,用于匹配校验
309+
- `artist` (可选): 歌手名称,用于匹配校验
304310

305311
**响应示例**:
306312

electron/server/unblock/bodian.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { SongUrlResult } from "./unblock";
1+
import type { SongMatchInfo, SongUrlResult } from "./unblock";
2+
import { isSongMatch } from "./match";
23
import { serverLog } from "../../main/logger";
34
import { createHash } from "crypto";
45
import axios from "axios";
@@ -57,12 +58,12 @@ const generateSign = (str: string) => {
5758

5859
/**
5960
* 搜索歌曲
60-
* @param keyword 搜索关键词
61+
* @param match 原曲匹配信息
6162
* @returns 歌曲 ID 或 null
6263
*/
63-
const search = async (info: string): Promise<string | null> => {
64+
const search = async (match: SongMatchInfo): Promise<string | null> => {
6465
try {
65-
const keyword = encodeURIComponent(info.replace(" - ", " "));
66+
const keyword = encodeURIComponent(match.keyword.replace(" - ", " "));
6667
const url =
6768
"http://search.kuwo.cn/r.s?&correct=1&vipver=1&stype=comprehensive&encoding=utf8" +
6869
"&rformat=json&mobi=1&show_copyright_off=1&searchapi=6&all=" +
@@ -76,10 +77,17 @@ const search = async (info: string): Promise<string | null> => {
7677
) {
7778
return null;
7879
}
79-
// 获取歌曲信息
80+
// 遍历搜索结果,找歌名和艺术家匹配的项
8081
const list = result.data.content[1].musicpage.abslist.map(format);
81-
if (list[0] && !list[0]?.id) return null;
82-
return list[0].id;
82+
for (const item of list) {
83+
if (!item?.id) continue;
84+
const artistStr = item.artists?.map((a: any) => a.name).join("&") || "";
85+
if (isSongMatch(item.name || "", artistStr, match)) {
86+
return item.id;
87+
}
88+
}
89+
serverLog.warn(`⚠️ Bodian 搜索结果均不匹配原曲: "${match.songName}"`);
90+
return null;
8391
} catch (error) {
8492
serverLog.error("❌ Get BodianSongId Error:", error);
8593
return null;
@@ -121,13 +129,13 @@ const sendAdFreeRequest = () => {
121129

122130
/**
123131
* 获取波点音乐歌曲 URL
124-
* @param keyword 搜索关键词
132+
* @param match 原曲匹配信息
125133
* @returns 包含歌曲 URL 的结果对象
126134
*/
127-
const getBodianSongUrl = async (keyword: string): Promise<SongUrlResult> => {
135+
const getBodianSongUrl = async (match: SongMatchInfo): Promise<SongUrlResult> => {
128136
try {
129-
if (!keyword) return { code: 404, url: null };
130-
const songId = await search(keyword);
137+
if (!match.keyword) return { code: 404, url: null };
138+
const songId = await search(match);
131139
if (!songId) return { code: 404, url: null };
132140
// 请求地址
133141
const headers = {

electron/server/unblock/gequbao.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,33 @@
1-
import { SongUrlResult } from "./unblock";
1+
import type { SongMatchInfo, SongUrlResult } from "./unblock";
2+
import { isSongMatch } from "./match";
23
import { serverLog } from "../../main/logger";
34
import axios from "axios";
45
import { randomBytes } from "crypto";
56

67
/**
78
* 搜索歌曲获取 ID
8-
* @param keyword 搜索关键词
9+
* @param match 原曲匹配信息
910
* @returns 歌曲 ID 或 null
1011
*/
11-
const search = async (keyword: string): Promise<string | null> => {
12+
const search = async (match: SongMatchInfo): Promise<string | null> => {
1213
try {
13-
const searchUrl = `https://www.gequbao.com/s/${encodeURIComponent(keyword)}`;
14+
const searchUrl = `https://www.gequbao.com/s/${encodeURIComponent(match.keyword)}`;
1415
const { data } = await axios.get(searchUrl);
1516

16-
// 匹配第一个歌曲链接 /music/12345
17+
// 匹配歌曲链接和歌名
1718
// <a href="/music/17165" target="_blank" class="music-link d-block">
18-
const match = data.match(
19-
/<a href="\/music\/(\d+)" target="_blank" class="music-link d-block">/,
20-
);
21-
if (match && match[1]) {
22-
return match[1];
19+
const regex = /<a href="\/music\/(\d+)" target="_blank" class="music-link d-block">\s*([^<]*)/g;
20+
let hasResult = false;
21+
// 校验歌名是否与原曲吻合(歌曲宝搜索页无艺术家信息,仅校验歌名)
22+
for (const m of data.matchAll(regex)) {
23+
hasResult = true;
24+
const songName = m[2]?.trim();
25+
if (songName && isSongMatch(songName, undefined, match)) {
26+
return m[1];
27+
}
2328
}
29+
if (!hasResult) return null;
30+
serverLog.warn(`⚠️ Gequbao 搜索结果均不匹配原曲: "${match.songName}"`);
2431
return null;
2532
} catch (error) {
2633
serverLog.error("❌ Get GequbaoSongId Error:", error);
@@ -53,15 +60,15 @@ const getPlayId = async (id: string): Promise<string | null> => {
5360

5461
/**
5562
* 获取歌曲 URL
56-
* @param keyword 搜索关键词
63+
* @param match 原曲匹配信息
5764
* @returns 包含歌曲 URL 的结果对象
5865
*/
59-
const getGequbaoSongUrl = async (keyword: string): Promise<SongUrlResult> => {
66+
const getGequbaoSongUrl = async (match: SongMatchInfo): Promise<SongUrlResult> => {
6067
try {
61-
if (!keyword) return { code: 404, url: null };
68+
if (!match.keyword) return { code: 404, url: null };
6269

6370
// 1. 获取 ID
64-
const id = await search(keyword);
71+
const id = await search(match);
6572
if (!id) return { code: 404, url: null };
6673

6774
// 2. 获取 play_id

electron/server/unblock/index.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
2-
import { SongUrlResult } from "./unblock";
2+
import type { SongUrlResult } from "./unblock";
33
import { serverLog } from "../../main/logger";
44
import axios from "axios";
55
import getKuwoSongUrl from "./kuwo";
@@ -51,15 +51,29 @@ export const initUnblockAPI = async (fastify: FastifyInstance) => {
5151
return reply.send(result);
5252
},
5353
);
54+
// 构造匹配信息(fallback 用 lastIndexOf 兼容歌名含连字符的情况)
55+
const buildMatchInfo = (query: { [key: string]: string }) => {
56+
let songName = query.songName || "";
57+
let artist = query.artist || "";
58+
if (!songName && query.keyword) {
59+
const lastIdx = query.keyword.lastIndexOf("-");
60+
if (lastIdx > 0) {
61+
songName = query.keyword.slice(0, lastIdx).trim();
62+
artist = artist || query.keyword.slice(lastIdx + 1).trim();
63+
} else {
64+
songName = query.keyword.trim();
65+
}
66+
}
67+
return { keyword: query.keyword || "", songName, artist };
68+
};
5469
// kuwo
5570
fastify.get(
5671
"/unblock/kuwo",
5772
async (
5873
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
5974
reply: FastifyReply,
6075
) => {
61-
const { keyword } = req.query;
62-
const result = await getKuwoSongUrl(keyword);
76+
const result = await getKuwoSongUrl(buildMatchInfo(req.query));
6377
return reply.send(result);
6478
},
6579
);
@@ -70,8 +84,7 @@ export const initUnblockAPI = async (fastify: FastifyInstance) => {
7084
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
7185
reply: FastifyReply,
7286
) => {
73-
const { keyword } = req.query;
74-
const result = await getBodianSongUrl(keyword);
87+
const result = await getBodianSongUrl(buildMatchInfo(req.query));
7588
return reply.send(result);
7689
},
7790
);
@@ -82,8 +95,7 @@ export const initUnblockAPI = async (fastify: FastifyInstance) => {
8295
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
8396
reply: FastifyReply,
8497
) => {
85-
const { keyword } = req.query;
86-
const result = await getGequbaoSongUrl(keyword);
98+
const result = await getGequbaoSongUrl(buildMatchInfo(req.query));
8799
return reply.send(result);
88100
},
89101
);

electron/server/unblock/kuwo.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { encryptQuery } from "./kwDES";
2-
import { SongUrlResult } from "./unblock";
2+
import type { SongMatchInfo, SongUrlResult } from "./unblock";
3+
import { isSongMatch } from "./match";
34
import { serverLog } from "../../main/logger";
45
import axios from "axios";
56

67
// 获取酷我音乐歌曲 ID
7-
const getKuwoSongId = async (keyword: string): Promise<string | null> => {
8+
const getKuwoSongId = async (match: SongMatchInfo): Promise<string | null> => {
89
try {
910
const url =
1011
"http://search.kuwo.cn/r.s?&correct=1&stype=comprehensive&encoding=utf8&rformat=json&mobi=1&show_copyright_off=1&searchapi=6&all=" +
11-
keyword;
12+
encodeURIComponent(match.keyword);
1213
const result = await axios.get(url);
1314
if (
1415
!result.data ||
@@ -18,24 +19,27 @@ const getKuwoSongId = async (keyword: string): Promise<string | null> => {
1819
) {
1920
return null;
2021
}
21-
// 获取歌曲信息
22-
const songId = result.data.content[1].musicpage.abslist[0].MUSICRID;
23-
const songName = result.data.content[1].musicpage.abslist[0]?.SONGNAME;
24-
// 是否与原曲吻合
25-
const originalName = keyword?.split("-") ?? keyword;
26-
if (songName && !songName?.includes(originalName[0])) return null;
27-
return songId.slice("MUSIC_".length);
22+
// 遍历搜索结果,找歌名和艺术家匹配的项
23+
for (const item of result.data.content[1].musicpage.abslist) {
24+
const songId = item?.MUSICRID;
25+
if (!songId) continue;
26+
if (isSongMatch(item?.SONGNAME || "", item?.ARTIST || "", match)) {
27+
return songId.slice("MUSIC_".length);
28+
}
29+
}
30+
serverLog.warn(`⚠️ Kuwo 搜索结果均不匹配原曲: "${match.songName}"`);
31+
return null;
2832
} catch (error) {
2933
serverLog.error("❌ Get KuwoSongId Error:", error);
3034
return null;
3135
}
3236
};
3337

3438
// 获取酷我音乐歌曲 URL
35-
const getKuwoSongUrl = async (keyword: string): Promise<SongUrlResult> => {
39+
const getKuwoSongUrl = async (match: SongMatchInfo): Promise<SongUrlResult> => {
3640
try {
37-
if (!keyword) return { code: 404, url: null };
38-
const songId = await getKuwoSongId(keyword);
41+
if (!match.keyword) return { code: 404, url: null };
42+
const songId = await getKuwoSongId(match);
3943
if (!songId) return { code: 404, url: null };
4044
// 请求地址
4145
const PackageName = "kwplayer_ar_5.1.0.0_B_jiakong_vh.apk";

electron/server/unblock/match.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { SongMatchInfo } from "./unblock";
2+
3+
/**
4+
* 归一化歌名用于匹配:小写 + 去除括号及其内容
5+
*/
6+
export const normalizeName = (name: string): string => {
7+
return name
8+
.toLowerCase()
9+
.replace(/[(][^)]*[)]/g, "")
10+
.trim();
11+
};
12+
13+
/**
14+
* 归一化艺术家名:小写 + 统一分隔符为空格
15+
*/
16+
export const normalizeArtist = (artist: string): string => {
17+
return artist
18+
.toLowerCase()
19+
.replace(/[&/,;]/g, " ")
20+
.replace(/\s+/g, " ")
21+
.trim();
22+
};
23+
24+
/**
25+
* 校验搜索结果是否与原曲匹配(歌名 + 艺术家)
26+
* @param resultName 搜索结果歌名
27+
* @param resultArtist 搜索结果艺术家(可选)
28+
* @param match 原曲匹配信息
29+
*/
30+
export const isSongMatch = (
31+
resultName: string,
32+
resultArtist: string | undefined,
33+
match: SongMatchInfo,
34+
): boolean => {
35+
const normalizedResult = normalizeName(resultName);
36+
const normalizedOriginal = normalizeName(match.songName);
37+
// songName 为空时跳过歌名检查(保持旧行为:不传 songName 则不限制歌名匹配)
38+
// normalizedResult 为空则视为无效结果,直接拒绝
39+
if (!normalizedResult) return false;
40+
if (normalizedOriginal) {
41+
// 歌名:双向 includes(兼容一方带后缀的情况)
42+
if (
43+
!normalizedResult.includes(normalizedOriginal) &&
44+
!normalizedOriginal.includes(normalizedResult)
45+
) {
46+
return false;
47+
}
48+
}
49+
// 艺术家:归一化分隔符后双向 includes
50+
if (resultArtist && match.artist) {
51+
const normalizedResultArtist = normalizeArtist(resultArtist);
52+
const normalizedOriginalArtist = normalizeArtist(match.artist);
53+
// 任一归一化后为空则跳过艺术家检查
54+
if (normalizedResultArtist && normalizedOriginalArtist) {
55+
if (
56+
!normalizedResultArtist.includes(normalizedOriginalArtist) &&
57+
!normalizedOriginalArtist.includes(normalizedResultArtist)
58+
) {
59+
return false;
60+
}
61+
}
62+
}
63+
return true;
64+
};

electron/server/unblock/unblock.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,10 @@ export type SongUrlResult = {
22
code: number;
33
url: string | null;
44
};
5+
6+
/** 用于校验搜索结果的原曲信息 */
7+
export type SongMatchInfo = {
8+
keyword: string;
9+
songName: string;
10+
artist: string;
11+
};

src/api/song.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,14 @@ export const songUrl = (
6363
};
6464

6565
// 获取解锁歌曲 URL
66-
export const unlockSongUrl = (id: number, keyword: string, server: SongUnlockServer) => {
67-
const params = server === SongUnlockServer.NETEASE ? { id } : { keyword };
66+
export const unlockSongUrl = (
67+
id: number,
68+
keyword: string,
69+
server: SongUnlockServer,
70+
songName?: string,
71+
artist?: string,
72+
) => {
73+
const params = server === SongUnlockServer.NETEASE ? { id } : { keyword, songName, artist };
6874
return request({
6975
baseURL: "/api/unblock",
7076
url: `/${server}`,

0 commit comments

Comments
 (0)