@zhin.js/plugin-music 7.0.7 → 7.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/cookie/delete/[source].js +23 -0
- package/commands/cookie/delete/[source].ts +27 -0
- package/commands/cookie/get/[source].js +29 -0
- package/commands/cookie/get/[source].ts +33 -0
- package/commands/cookie/set/[source].js +23 -0
- package/commands/cookie/set/[source].ts +27 -0
- package/commands/cookie/status.js +25 -0
- package/commands/cookie/status.ts +25 -0
- package/commands/login/[platform].js +64 -0
- package/commands/login/[platform].ts +76 -0
- package/commands//347/202/271/346/255/214/[keyword].js +62 -0
- package/commands//347/202/271/346/255/214/[keyword].ts +74 -0
- package/components/share-music.js +6 -6
- package/components/share-music.ts +6 -6
- package/lib/config.d.ts +3 -3
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +36 -7
- package/lib/config.js.map +1 -1
- package/lib/credential-store.d.ts +34 -0
- package/lib/credential-store.d.ts.map +1 -0
- package/lib/credential-store.js +111 -0
- package/lib/credential-store.js.map +1 -0
- package/lib/index.d.ts +5 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +5 -2
- package/lib/index.js.map +1 -1
- package/lib/login/index.d.ts +20 -0
- package/lib/login/index.d.ts.map +1 -0
- package/lib/login/index.js +100 -0
- package/lib/login/index.js.map +1 -0
- package/lib/login/netease.d.ts +6 -0
- package/lib/login/netease.d.ts.map +1 -0
- package/lib/login/netease.js +58 -0
- package/lib/login/netease.js.map +1 -0
- package/lib/login/qq.d.ts +7 -0
- package/lib/login/qq.d.ts.map +1 -0
- package/lib/login/qq.js +139 -0
- package/lib/login/qq.js.map +1 -0
- package/lib/login/types.d.ts +19 -0
- package/lib/login/types.d.ts.map +1 -0
- package/lib/login/types.js +11 -0
- package/lib/login/types.js.map +1 -0
- package/lib/music-lib.d.ts +13 -12
- package/lib/music-lib.d.ts.map +1 -1
- package/lib/music-lib.js +36 -22
- package/lib/music-lib.js.map +1 -1
- package/lib/session.d.ts +29 -0
- package/lib/session.d.ts.map +1 -0
- package/lib/session.js +39 -0
- package/lib/session.js.map +1 -0
- package/lib/sources/index.d.ts +2 -1
- package/lib/sources/index.d.ts.map +1 -1
- package/lib/sources/index.js +6 -2
- package/lib/sources/index.js.map +1 -1
- package/lib/sources/kugou.d.ts +6 -0
- package/lib/sources/kugou.d.ts.map +1 -0
- package/lib/sources/kugou.js +54 -0
- package/lib/sources/kugou.js.map +1 -0
- package/lib/sources/kuwo.d.ts +7 -0
- package/lib/sources/kuwo.d.ts.map +1 -0
- package/lib/sources/kuwo.js +69 -0
- package/lib/sources/kuwo.js.map +1 -0
- package/lib/sources/netease.d.ts +1 -1
- package/lib/sources/netease.d.ts.map +1 -1
- package/lib/sources/netease.js +19 -5
- package/lib/sources/netease.js.map +1 -1
- package/lib/sources/qq.d.ts +1 -14
- package/lib/sources/qq.d.ts.map +1 -1
- package/lib/sources/qq.js +114 -81
- package/lib/sources/qq.js.map +1 -1
- package/lib/types.d.ts +4 -24
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +0 -1
- package/lib/types.js.map +1 -1
- package/middlewares/music-selection.js +46 -0
- package/middlewares/music-selection.ts +56 -0
- package/package.json +10 -5
- package/plugin.js +24 -1
- package/src/config.ts +54 -30
- package/src/credential-store.ts +169 -0
- package/src/index.ts +43 -2
- package/src/login/index.ts +135 -0
- package/src/login/netease.ts +69 -0
- package/src/login/qq.ts +154 -0
- package/src/login/types.ts +27 -0
- package/src/music-lib.ts +39 -26
- package/src/session.ts +60 -0
- package/src/sources/index.ts +12 -8
- package/src/sources/kugou.ts +78 -0
- package/src/sources/kuwo.ts +94 -0
- package/src/sources/netease.ts +29 -12
- package/src/sources/qq.ts +158 -125
- package/src/types.ts +6 -28
- package/tools/music-search.js +6 -2
- package/tools/music-search.ts +6 -2
- package/tools/music-share.js +6 -2
- package/tools/music-share.ts +6 -2
package/src/session.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { MusicInfo, MusicSource } from './types.js';
|
|
2
|
+
|
|
3
|
+
export interface PendingSearch {
|
|
4
|
+
readonly results: readonly MusicInfo[];
|
|
5
|
+
readonly source: MusicSource;
|
|
6
|
+
readonly keyword: string;
|
|
7
|
+
readonly timestamp: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const SEARCH_TIMEOUT_MS = 3 * 60 * 1000;
|
|
11
|
+
|
|
12
|
+
const pendingSessions = new Map<string, PendingSearch>();
|
|
13
|
+
|
|
14
|
+
export function sessionKey(
|
|
15
|
+
endpointId: string,
|
|
16
|
+
conversationId: string,
|
|
17
|
+
senderId: string,
|
|
18
|
+
): string {
|
|
19
|
+
return `${endpointId}:${conversationId}:${senderId}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveMessageIds(input: {
|
|
23
|
+
metadata?: Record<string, unknown>;
|
|
24
|
+
conversation?: { id?: unknown; endpoint?: { id?: unknown } };
|
|
25
|
+
sender?: { id?: unknown };
|
|
26
|
+
}): { endpointId: string; conversationId: string; senderId: string } | null {
|
|
27
|
+
const meta = input.metadata ?? {};
|
|
28
|
+
const endpointId = String(meta.endpointId ?? input.conversation?.endpoint?.id ?? '');
|
|
29
|
+
const conversationId = String(input.conversation?.id ?? '');
|
|
30
|
+
const senderId = String(input.sender?.id ?? '');
|
|
31
|
+
if (!endpointId || !conversationId || !senderId) return null;
|
|
32
|
+
return { endpointId, conversationId, senderId };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function setPending(key: string, search: PendingSearch): void {
|
|
36
|
+
pendingSessions.set(key, search);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getPending(key: string): PendingSearch | undefined {
|
|
40
|
+
const session = pendingSessions.get(key);
|
|
41
|
+
if (!session) return undefined;
|
|
42
|
+
if (Date.now() - session.timestamp > SEARCH_TIMEOUT_MS) {
|
|
43
|
+
pendingSessions.delete(key);
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return session;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function clearPending(key: string): void {
|
|
50
|
+
pendingSessions.delete(key);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function cleanExpired(): void {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
for (const [key, session] of pendingSessions) {
|
|
56
|
+
if (now - session.timestamp > SEARCH_TIMEOUT_MS) {
|
|
57
|
+
pendingSessions.delete(key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/sources/index.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
1
|
+
import { QQMusicService } from './qq.js';
|
|
2
|
+
import { NeteaseMusicService } from './netease.js';
|
|
3
|
+
import { KuwoMusicService } from './kuwo.js';
|
|
4
|
+
import { KugouMusicService } from './kugou.js';
|
|
5
|
+
import type { MusicSource, MusicSearchService } from '../types.js';
|
|
5
6
|
|
|
6
|
-
/** 音乐源服务映射 */
|
|
7
7
|
export const musicServices: Record<MusicSource, MusicSearchService> = {
|
|
8
8
|
qq: new QQMusicService(),
|
|
9
9
|
netease: new NeteaseMusicService(),
|
|
10
|
-
|
|
10
|
+
kuwo: new KuwoMusicService(),
|
|
11
|
+
kugou: new KugouMusicService(),
|
|
12
|
+
};
|
|
11
13
|
|
|
12
|
-
export * from './qq.js'
|
|
13
|
-
export * from './netease.js'
|
|
14
|
+
export * from './qq.js';
|
|
15
|
+
export * from './netease.js';
|
|
16
|
+
export * from './kuwo.js';
|
|
17
|
+
export * from './kugou.js';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { MusicSearchService, MusicDetail, MusicInfo } from '../types.js';
|
|
2
|
+
|
|
3
|
+
interface KugouSearchItem {
|
|
4
|
+
hash: string;
|
|
5
|
+
songname: string;
|
|
6
|
+
singername: string;
|
|
7
|
+
album_name?: string;
|
|
8
|
+
duration: number;
|
|
9
|
+
album_id?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class KugouMusicService implements MusicSearchService {
|
|
13
|
+
async search(keyword: string, limit = 10): Promise<MusicInfo[]> {
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL('https://mobilecdn.kugou.com/api/v3/search/song');
|
|
16
|
+
url.searchParams.set('format', 'json');
|
|
17
|
+
url.searchParams.set('keyword', keyword);
|
|
18
|
+
url.searchParams.set('page', '1');
|
|
19
|
+
url.searchParams.set('pagesize', String(limit));
|
|
20
|
+
url.searchParams.set('showtype', '1');
|
|
21
|
+
|
|
22
|
+
const response = await fetch(url, { method: 'GET' });
|
|
23
|
+
const data = (await response.json()) as {
|
|
24
|
+
data?: { info?: KugouSearchItem[] };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const items = data.data?.info ?? [];
|
|
28
|
+
return items.slice(0, limit).map((item) => ({
|
|
29
|
+
id: item.hash,
|
|
30
|
+
source: 'kugou' as const,
|
|
31
|
+
title: item.songname?.replace(/<[^>]+>/g, '') ?? '',
|
|
32
|
+
artist: item.singername,
|
|
33
|
+
album: item.album_name,
|
|
34
|
+
duration: item.duration,
|
|
35
|
+
url: `https://www.kugou.com/song/#hash=${item.hash}`,
|
|
36
|
+
}));
|
|
37
|
+
} catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async getDetail(id: string): Promise<MusicDetail> {
|
|
43
|
+
const url = new URL('https://wwwapi.kugou.com/yy/index.php');
|
|
44
|
+
url.searchParams.set('r', 'play/getdata');
|
|
45
|
+
url.searchParams.set('hash', id);
|
|
46
|
+
url.searchParams.set('mid', 'music');
|
|
47
|
+
url.searchParams.set('platid', '4');
|
|
48
|
+
|
|
49
|
+
const response = await fetch(url, { method: 'GET' });
|
|
50
|
+
const result = (await response.json()) as {
|
|
51
|
+
data?: {
|
|
52
|
+
song_name?: string;
|
|
53
|
+
author_name?: string;
|
|
54
|
+
album_name?: string;
|
|
55
|
+
img?: string;
|
|
56
|
+
play_url?: string;
|
|
57
|
+
timelength?: number;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
const info = result.data;
|
|
61
|
+
if (!info) throw new Error('Music not found');
|
|
62
|
+
|
|
63
|
+
const audio = info.play_url ?? '';
|
|
64
|
+
if (!audio) throw new Error('Audio URL not found');
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
id,
|
|
68
|
+
source: 'kugou',
|
|
69
|
+
title: info.song_name ?? '',
|
|
70
|
+
artist: info.author_name,
|
|
71
|
+
album: info.album_name,
|
|
72
|
+
url: `https://www.kugou.com/song/#hash=${id}`,
|
|
73
|
+
image: info.img ?? '',
|
|
74
|
+
audio,
|
|
75
|
+
duration: info.timelength ? Math.floor(info.timelength / 1000) : undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { MusicSearchService, MusicDetail, MusicInfo } from '../types.js';
|
|
2
|
+
|
|
3
|
+
interface KuwoSearchItem {
|
|
4
|
+
rid: number;
|
|
5
|
+
name: string;
|
|
6
|
+
artist: string;
|
|
7
|
+
album: string;
|
|
8
|
+
duration: number;
|
|
9
|
+
pic?: string;
|
|
10
|
+
albumpic?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class KuwoMusicService implements MusicSearchService {
|
|
14
|
+
async search(keyword: string, limit = 10): Promise<MusicInfo[]> {
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL('https://search.kuwo.cn/r.s');
|
|
17
|
+
url.searchParams.set('client', 'kt');
|
|
18
|
+
url.searchParams.set('all', keyword);
|
|
19
|
+
url.searchParams.set('pn', '0');
|
|
20
|
+
url.searchParams.set('rn', String(limit));
|
|
21
|
+
url.searchParams.set('ft', 'music');
|
|
22
|
+
url.searchParams.set('encoding', 'utf8');
|
|
23
|
+
url.searchParams.set('rformat', 'json');
|
|
24
|
+
url.searchParams.set('mobi', '1');
|
|
25
|
+
url.searchParams.set('vipver', '1');
|
|
26
|
+
|
|
27
|
+
const response = await fetch(url, { method: 'GET' });
|
|
28
|
+
const text = await response.text();
|
|
29
|
+
const data = JSON.parse(text.replace(/'/g, '"')) as {
|
|
30
|
+
abslist?: KuwoSearchItem[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const items = data.abslist ?? [];
|
|
34
|
+
return items.slice(0, limit).map((item) => ({
|
|
35
|
+
id: String(item.rid),
|
|
36
|
+
source: 'kuwo' as const,
|
|
37
|
+
title: item.name?.replace(/ /g, ' ') ?? '',
|
|
38
|
+
artist: item.artist?.replace(/ /g, ' '),
|
|
39
|
+
album: item.album?.replace(/ /g, ' '),
|
|
40
|
+
duration: item.duration,
|
|
41
|
+
url: `https://www.kuwo.cn/play_detail/${item.rid}`,
|
|
42
|
+
}));
|
|
43
|
+
} catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getDetail(id: string): Promise<MusicDetail> {
|
|
49
|
+
const url = new URL('https://www.kuwo.cn/api/www/music/musicInfo');
|
|
50
|
+
url.searchParams.set('mid', id);
|
|
51
|
+
url.searchParams.set('httpsStatus', '1');
|
|
52
|
+
|
|
53
|
+
const response = await fetch(url, {
|
|
54
|
+
method: 'GET',
|
|
55
|
+
headers: { Referer: 'https://www.kuwo.cn/', csrf: id },
|
|
56
|
+
});
|
|
57
|
+
const result = (await response.json()) as {
|
|
58
|
+
data?: {
|
|
59
|
+
name?: string;
|
|
60
|
+
artist?: string;
|
|
61
|
+
album?: string;
|
|
62
|
+
pic?: string;
|
|
63
|
+
albumpic?: string;
|
|
64
|
+
duration?: number;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
const info = result.data;
|
|
68
|
+
if (!info) throw new Error('Music not found');
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
id,
|
|
72
|
+
source: 'kuwo',
|
|
73
|
+
title: info.name ?? '',
|
|
74
|
+
artist: info.artist,
|
|
75
|
+
album: info.album,
|
|
76
|
+
url: `https://www.kuwo.cn/play_detail/${id}`,
|
|
77
|
+
image: info.pic ?? info.albumpic ?? '',
|
|
78
|
+
audio: await this.getAudioUrl(id),
|
|
79
|
+
duration: info.duration,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async getAudioUrl(id: string): Promise<string> {
|
|
84
|
+
const url = `https://antiserver.kuwo.cn/anti.s?type=convert_url3&rid=${id}&format=mp3`;
|
|
85
|
+
const response = await fetch(url, {
|
|
86
|
+
method: 'GET',
|
|
87
|
+
redirect: 'follow',
|
|
88
|
+
});
|
|
89
|
+
const text = await response.text();
|
|
90
|
+
const match = /http[^\s]+/.exec(text);
|
|
91
|
+
if (match) return match[0];
|
|
92
|
+
throw new Error('Audio URL not found');
|
|
93
|
+
}
|
|
94
|
+
}
|
package/src/sources/netease.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// plugins/utils/music/src/sources/netease.ts
|
|
2
|
-
import type { MusicSearchService, MusicDetail,MusicInfo, Music163 } from '../types.js'
|
|
2
|
+
import type { MusicSearchService, MusicDetail, MusicInfo, Music163 } from '../types.js';
|
|
3
|
+
import { getCredential } from '../credential-store.js';
|
|
3
4
|
|
|
4
5
|
/** 网易云音乐搜索服务 */
|
|
5
6
|
export class NeteaseMusicService implements MusicSearchService {
|
|
@@ -70,16 +71,32 @@ export class NeteaseMusicService implements MusicSearchService {
|
|
|
70
71
|
* @param metingAPI Meting API 地址(可选)
|
|
71
72
|
* @returns 音频直链 URL
|
|
72
73
|
*/
|
|
73
|
-
async getAudioUrl(id: string
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
async getAudioUrl(id: string): Promise<string> {
|
|
75
|
+
const cookie = await getCredential('netease', 'cookie');
|
|
76
|
+
if (cookie) {
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(
|
|
79
|
+
`https://music.163.com/api/song/enhance/player/url?ids=[${id}]&br=320000`,
|
|
80
|
+
{
|
|
81
|
+
method: 'GET',
|
|
82
|
+
headers: { Cookie: cookie },
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
const result = await response.json() as {
|
|
86
|
+
data?: Array<{ url?: string | null }>;
|
|
87
|
+
};
|
|
88
|
+
const audioUrl = result.data?.[0]?.url;
|
|
89
|
+
if (audioUrl) return audioUrl;
|
|
90
|
+
} catch {
|
|
91
|
+
// cookie-based fetch failed, fall through to public API
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const apiUrl = 'https://api.injahow.cn/meting/';
|
|
95
|
+
const url = `${apiUrl}?type=url&id=${id}`;
|
|
96
|
+
const response = await fetch(url, { method: 'GET' });
|
|
97
|
+
const data = await response.json() as { url?: string; data?: { url?: string } };
|
|
98
|
+
const audioUrl = data.url ?? data.data?.url;
|
|
99
|
+
if (!audioUrl) throw new Error('Audio URL not found');
|
|
100
|
+
return audioUrl;
|
|
84
101
|
}
|
|
85
102
|
}
|
package/src/sources/qq.ts
CHANGED
|
@@ -1,147 +1,180 @@
|
|
|
1
|
-
|
|
2
|
-
import type { MusicSearchService, MusicDetail,MusicInfo
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import type { MusicSearchService, MusicDetail, MusicInfo } from '../types.js';
|
|
3
|
+
import { getCredential } from '../credential-store.js';
|
|
4
|
+
|
|
5
|
+
const md5 = (text: string) => crypto.createHash('md5').update(text).digest('hex');
|
|
6
|
+
|
|
7
|
+
const MUSICU_API = 'https://u.y.qq.com/cgi-bin/musicu.fcg';
|
|
8
|
+
|
|
9
|
+
function getHeaders(cookie?: string | null) {
|
|
10
|
+
return {
|
|
11
|
+
Referer: 'https://y.qq.com',
|
|
12
|
+
Cookie: cookie ?? '',
|
|
13
|
+
'Content-Type': 'application/json; charset=UTF-8',
|
|
14
|
+
};
|
|
15
|
+
}
|
|
3
16
|
|
|
4
|
-
/** QQ 音乐搜索服务 */
|
|
5
17
|
export class QQMusicService implements MusicSearchService {
|
|
6
18
|
async search(keyword: string, limit = 10): Promise<MusicInfo[]> {
|
|
7
19
|
try {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
const cookie = await getCredential('qq', 'cookie');
|
|
21
|
+
const response = await fetch(MUSICU_API, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: getHeaders(cookie),
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
comm: { uin: '0', authst: '', ct: 29 },
|
|
26
|
+
search: {
|
|
27
|
+
method: 'DoSearchForQQMusicMobile',
|
|
28
|
+
module: 'music.search.SearchCgiService',
|
|
29
|
+
param: {
|
|
30
|
+
query: keyword,
|
|
31
|
+
grp: 1,
|
|
32
|
+
num_per_page: limit,
|
|
33
|
+
page_num: 1,
|
|
34
|
+
search_type: 0,
|
|
35
|
+
searchid: String(Date.now()),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
27
40
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
41
|
+
const data = (await response.json()) as { code?: number; search?: { data?: { body?: { song?: { list?: any[] }; item_song?: any[] } } } };
|
|
42
|
+
if (data.code !== undefined && +data.code !== 0) return [];
|
|
43
|
+
|
|
44
|
+
const body = data.search?.data?.body ?? {};
|
|
45
|
+
const list = (body.song?.list ?? body.item_song ?? []) as any[];
|
|
46
|
+
|
|
47
|
+
return list.slice(0, limit).map((e) => {
|
|
48
|
+
const mid = e?.mid ?? '';
|
|
49
|
+
const albumMid = e?.album?.pmid ?? e?.album?.mid ?? '';
|
|
50
|
+
const singerMid = e?.singer?.[0]?.pmid ?? e?.singer?.[0]?.mid ?? '';
|
|
51
|
+
const cover = albumMid
|
|
52
|
+
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${albumMid}.jpg`
|
|
53
|
+
: singerMid
|
|
54
|
+
? `https://y.qq.com/music/photo_new/T001R300x300M000${singerMid}.jpg`
|
|
55
|
+
: '';
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
id: String(e.id),
|
|
59
|
+
source: 'qq' as const,
|
|
60
|
+
title: e.name,
|
|
61
|
+
artist: (e.singer ?? []).map((s: any) => s.name).join('/'),
|
|
62
|
+
album: e.album?.name,
|
|
63
|
+
url: `https://y.qq.com/n/ryqq/songDetail/${mid}`,
|
|
64
|
+
image: cover,
|
|
65
|
+
duration: e.interval,
|
|
66
|
+
_mid: mid,
|
|
67
|
+
_mediaMid: e.file?.media_mid,
|
|
68
|
+
};
|
|
69
|
+
});
|
|
53
70
|
} catch (error) {
|
|
54
|
-
console.error('QQ Music
|
|
55
|
-
return
|
|
71
|
+
console.error('QQ Music search failed:', error);
|
|
72
|
+
return [];
|
|
56
73
|
}
|
|
57
74
|
}
|
|
58
75
|
|
|
59
76
|
async getDetail(id: string): Promise<MusicDetail> {
|
|
60
|
-
const url = new URL(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
77
|
+
const url = new URL(MUSICU_API);
|
|
78
|
+
url.searchParams.set('format', 'json');
|
|
79
|
+
url.searchParams.set('data', JSON.stringify({
|
|
80
|
+
comm: { ct: 24, cv: 0 },
|
|
81
|
+
songinfo: {
|
|
82
|
+
method: 'get_song_detail_yqq',
|
|
83
|
+
param: { song_type: 0, song_mid: '', song_id: parseInt(id) },
|
|
84
|
+
module: 'music.pf_song_detail_svr',
|
|
85
|
+
},
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
const response = await fetch(url, { method: 'GET' });
|
|
89
|
+
const result = await response.json() as any;
|
|
90
|
+
|
|
91
|
+
const trackInfo = result?.songinfo?.data?.track_info;
|
|
92
|
+
if (!trackInfo) throw new Error('Music not found');
|
|
93
|
+
|
|
94
|
+
const albumMid = trackInfo.album?.mid;
|
|
95
|
+
const image = albumMid
|
|
96
|
+
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${albumMid}.jpg`
|
|
97
|
+
: '';
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
id,
|
|
101
|
+
source: 'qq',
|
|
102
|
+
title: trackInfo.name,
|
|
103
|
+
url: `https://y.qq.com/n/ryqq/songDetail/${trackInfo.mid}`,
|
|
104
|
+
audio: await this.getAudioUrl(id, trackInfo.mid, trackInfo.file?.media_mid),
|
|
105
|
+
image,
|
|
106
|
+
duration: trackInfo.interval,
|
|
107
|
+
};
|
|
91
108
|
}
|
|
92
109
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
async getAudioUrl(id: string, mid?: string, mediaMid?: string): Promise<string> {
|
|
111
|
+
const cookie = await getCredential('qq', 'cookie');
|
|
112
|
+
if (cookie) {
|
|
113
|
+
try {
|
|
114
|
+
const uin = /uin=o?(\d+)/.exec(cookie)?.[1] ?? '0';
|
|
115
|
+
const songMid = mid ?? id;
|
|
116
|
+
const response = await fetch(MUSICU_API, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: getHeaders(cookie),
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
comm: {
|
|
121
|
+
ct: 19,
|
|
122
|
+
cv: '1891',
|
|
123
|
+
uin: '0',
|
|
124
|
+
guid: md5(`${uin}music`),
|
|
125
|
+
tmeAppID: 'qqmusic',
|
|
126
|
+
tmeLoginType: 2,
|
|
127
|
+
},
|
|
128
|
+
vkey: {
|
|
129
|
+
module: 'vkey.GetVkeyServer',
|
|
130
|
+
method: 'CgiGetVkey',
|
|
131
|
+
param: {
|
|
132
|
+
guid: md5(String(Date.now())),
|
|
133
|
+
songmid: [songMid],
|
|
134
|
+
songtype: [0],
|
|
135
|
+
uin: '0',
|
|
136
|
+
ctx: 1,
|
|
137
|
+
filename: [`C400${id}${mediaMid ?? songMid}.m4a`],
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const result = await response.json() as any;
|
|
144
|
+
if (result.vkey && +result.vkey.code === 0) {
|
|
145
|
+
const purl = result.vkey.data?.midurlinfo?.[0]?.purl;
|
|
146
|
+
if (purl) return `https://ws.stream.qqmusic.qq.com/${purl}`;
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// fall through to public fallback
|
|
108
150
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 无 cookie 的公开 fallback:md5 签名直链
|
|
154
|
+
const songMid = mid ?? id;
|
|
155
|
+
const code = md5(`${songMid}q;z(&l~sdf2!nK`).substring(0, 5).toUpperCase();
|
|
156
|
+
return `https://c6.y.qq.com/rsc/fcgi-bin/fcg_pyq_play.fcg?songmid=${songMid}&code=${code}`;
|
|
112
157
|
}
|
|
113
158
|
|
|
114
|
-
/**
|
|
115
|
-
* 获取歌词
|
|
116
|
-
* @param id 音乐 ID
|
|
117
|
-
* @returns 歌词文本
|
|
118
|
-
*/
|
|
119
159
|
async getLyric(id: string): Promise<string | null> {
|
|
120
160
|
try {
|
|
121
|
-
const url = new URL('https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg')
|
|
122
|
-
url.searchParams.set('songmid', id)
|
|
123
|
-
url.searchParams.set('g_tk', '5381')
|
|
124
|
-
url.searchParams.set('format', 'json')
|
|
125
|
-
url.searchParams.set('inCharset', 'utf8')
|
|
126
|
-
url.searchParams.set('outCharset', 'utf-8')
|
|
127
|
-
url.searchParams.set('nobase64', '1')
|
|
128
|
-
|
|
129
|
-
const response = await fetch(url, {
|
|
161
|
+
const url = new URL('https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg');
|
|
162
|
+
url.searchParams.set('songmid', id);
|
|
163
|
+
url.searchParams.set('g_tk', '5381');
|
|
164
|
+
url.searchParams.set('format', 'json');
|
|
165
|
+
url.searchParams.set('inCharset', 'utf8');
|
|
166
|
+
url.searchParams.set('outCharset', 'utf-8');
|
|
167
|
+
url.searchParams.set('nobase64', '1');
|
|
168
|
+
|
|
169
|
+
const response = await fetch(url, {
|
|
130
170
|
method: 'GET',
|
|
131
|
-
headers: {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const data = await response.json() as { lyric?: string }
|
|
136
|
-
|
|
137
|
-
if (!data.lyric) return null
|
|
138
|
-
|
|
139
|
-
// 请求已带 nobase64=1,返回的是明文歌词,直接返回;
|
|
140
|
-
// 再按 base64 解码会得到乱码(Buffer.from(x,'base64') 对明文也不会抛错)
|
|
141
|
-
return data.lyric
|
|
171
|
+
headers: { Referer: 'https://y.qq.com' },
|
|
172
|
+
});
|
|
173
|
+
const data = (await response.json()) as { lyric?: string };
|
|
174
|
+
return data.lyric ?? null;
|
|
142
175
|
} catch (error) {
|
|
143
|
-
console.error('QQ Music get lyric failed:', error)
|
|
144
|
-
return null
|
|
176
|
+
console.error('QQ Music get lyric failed:', error);
|
|
177
|
+
return null;
|
|
145
178
|
}
|
|
146
179
|
}
|
|
147
180
|
}
|