@zhin.js/plugin-music 1.0.26 → 1.1.2
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/README.md +28 -27
- package/commands/cookie/delete/[source]/index.js +23 -0
- package/commands/cookie/delete/[source]/index.ts +27 -0
- package/commands/cookie/get/[source]/index.js +29 -0
- package/commands/cookie/get/[source]/index.ts +33 -0
- package/commands/cookie/set/[source]/index.js +23 -0
- package/commands/cookie/set/[source]/index.ts +27 -0
- package/commands/cookie/status/index.js +25 -0
- package/commands/cookie/status/index.ts +25 -0
- package/commands/login/[platform]/index.js +66 -0
- package/commands/login/[platform]/index.ts +74 -0
- package/commands//347/202/271/346/255/214/[keyword]/index.js +63 -0
- package/commands//347/202/271/346/255/214/[keyword]/index.ts +75 -0
- package/components/share-music/index.js +24 -0
- package/components/share-music/index.ts +29 -0
- 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 +37 -0
- package/lib/credential-store.d.ts.map +1 -0
- package/lib/credential-store.js +85 -0
- package/lib/credential-store.js.map +1 -0
- package/lib/index.d.ts +8 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +7 -112
- package/lib/index.js.map +1 -1
- package/lib/login/index.d.ts +27 -0
- package/lib/login/index.d.ts.map +1 -0
- package/lib/login/index.js +124 -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 +32 -0
- package/lib/music-lib.d.ts.map +1 -0
- package/lib/music-lib.js +58 -0
- package/lib/music-lib.js.map +1 -0
- package/lib/runtime.d.ts +12 -0
- package/lib/runtime.d.ts.map +1 -0
- package/lib/runtime.js +3 -0
- package/lib/runtime.js.map +1 -0
- package/lib/session.d.ts +35 -0
- package/lib/session.d.ts.map +1 -0
- package/lib/session.js +51 -0
- package/lib/session.js.map +1 -0
- package/lib/sources/index.d.ts +4 -2
- package/lib/sources/index.d.ts.map +1 -1
- package/lib/sources/index.js +12 -6
- 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 +69 -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 +4 -1
- package/lib/sources/netease.d.ts.map +1 -1
- package/lib/sources/netease.js +25 -6
- package/lib/sources/netease.js.map +1 -1
- package/lib/sources/qq.d.ts +4 -14
- package/lib/sources/qq.d.ts.map +1 -1
- package/lib/sources/qq.js +117 -82
- 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/index.js +48 -0
- package/middlewares/music-selection/index.ts +62 -0
- package/package.json +66 -12
- package/plugin.js +45 -0
- package/schema.json +6 -0
- package/skills/music/SKILL.md +2 -2
- package/skills/music/tools/music-search/index.js +22 -0
- package/skills/music/tools/music-search/index.ts +33 -0
- package/skills/music/tools/music-share/index.js +21 -0
- package/skills/music/tools/music-share/index.ts +26 -0
- package/src/config.ts +54 -30
- package/src/credential-store.ts +125 -0
- package/src/index.ts +37 -0
- package/src/login/index.ts +154 -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 +71 -0
- package/src/runtime.ts +17 -0
- package/src/session.ts +77 -0
- package/src/sources/index.ts +17 -10
- package/src/sources/kugou.ts +92 -0
- package/src/sources/kuwo.ts +94 -0
- package/src/sources/netease.ts +35 -11
- package/src/sources/qq.ts +164 -126
- package/src/types.ts +6 -28
- package/lib/async-jsx.d.ts +0 -19
- package/lib/async-jsx.d.ts.map +0 -1
- package/lib/async-jsx.js +0 -48
- package/lib/async-jsx.js.map +0 -1
- package/src/async-jsx.ts +0 -54
- package/src/index.tsx +0 -130
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
function stripHtmlTags(input: string): string {
|
|
13
|
+
let result = '';
|
|
14
|
+
let cursor = 0;
|
|
15
|
+
while (cursor < input.length) {
|
|
16
|
+
const start = input.indexOf('<', cursor);
|
|
17
|
+
if (start < 0) return result + input.slice(cursor);
|
|
18
|
+
result += input.slice(cursor, start);
|
|
19
|
+
const end = input.indexOf('>', start + 1);
|
|
20
|
+
if (end < 0) return result + input.slice(start);
|
|
21
|
+
cursor = end + 1;
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class KugouMusicService implements MusicSearchService {
|
|
27
|
+
async search(keyword: string, limit = 10): Promise<MusicInfo[]> {
|
|
28
|
+
try {
|
|
29
|
+
const url = new URL('https://mobilecdn.kugou.com/api/v3/search/song');
|
|
30
|
+
url.searchParams.set('format', 'json');
|
|
31
|
+
url.searchParams.set('keyword', keyword);
|
|
32
|
+
url.searchParams.set('page', '1');
|
|
33
|
+
url.searchParams.set('pagesize', String(limit));
|
|
34
|
+
url.searchParams.set('showtype', '1');
|
|
35
|
+
|
|
36
|
+
const response = await fetch(url, { method: 'GET' });
|
|
37
|
+
const data = (await response.json()) as {
|
|
38
|
+
data?: { info?: KugouSearchItem[] };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const items = data.data?.info ?? [];
|
|
42
|
+
return items.slice(0, limit).map((item) => ({
|
|
43
|
+
id: item.hash,
|
|
44
|
+
source: 'kugou' as const,
|
|
45
|
+
title: stripHtmlTags(item.songname ?? ''),
|
|
46
|
+
artist: item.singername,
|
|
47
|
+
album: item.album_name,
|
|
48
|
+
duration: item.duration,
|
|
49
|
+
url: `https://www.kugou.com/song/#hash=${item.hash}`,
|
|
50
|
+
}));
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async getDetail(id: string): Promise<MusicDetail> {
|
|
57
|
+
const url = new URL('https://wwwapi.kugou.com/yy/index.php');
|
|
58
|
+
url.searchParams.set('r', 'play/getdata');
|
|
59
|
+
url.searchParams.set('hash', id);
|
|
60
|
+
url.searchParams.set('mid', 'music');
|
|
61
|
+
url.searchParams.set('platid', '4');
|
|
62
|
+
|
|
63
|
+
const response = await fetch(url, { method: 'GET' });
|
|
64
|
+
const result = (await response.json()) as {
|
|
65
|
+
data?: {
|
|
66
|
+
song_name?: string;
|
|
67
|
+
author_name?: string;
|
|
68
|
+
album_name?: string;
|
|
69
|
+
img?: string;
|
|
70
|
+
play_url?: string;
|
|
71
|
+
timelength?: number;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
const info = result.data;
|
|
75
|
+
if (!info) throw new Error('Music not found');
|
|
76
|
+
|
|
77
|
+
const audio = info.play_url ?? '';
|
|
78
|
+
if (!audio) throw new Error('Audio URL not found');
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
id,
|
|
82
|
+
source: 'kugou',
|
|
83
|
+
title: info.song_name ?? '',
|
|
84
|
+
artist: info.author_name,
|
|
85
|
+
album: info.album_name,
|
|
86
|
+
url: `https://www.kugou.com/song/#hash=${id}`,
|
|
87
|
+
image: info.img ?? '',
|
|
88
|
+
audio,
|
|
89
|
+
duration: info.timelength ? Math.floor(info.timelength / 1000) : undefined,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -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,8 +1,15 @@
|
|
|
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 type { CredentialStore } from '../credential-store.js';
|
|
3
4
|
|
|
4
5
|
/** 网易云音乐搜索服务 */
|
|
5
6
|
export class NeteaseMusicService implements MusicSearchService {
|
|
7
|
+
readonly #credentials: CredentialStore;
|
|
8
|
+
|
|
9
|
+
constructor(credentials: CredentialStore) {
|
|
10
|
+
this.#credentials = credentials;
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
async search(keyword: string, limit = 10): Promise<MusicInfo[]> {
|
|
7
14
|
try {
|
|
8
15
|
const searchUrl = `http://music.163.com/api/search/get/web?csrf_token=hlpretag=&hlposttag=&s=${encodeURIComponent(keyword)}&type=1&offset=0&total=true&limit=${limit}`
|
|
@@ -70,15 +77,32 @@ export class NeteaseMusicService implements MusicSearchService {
|
|
|
70
77
|
* @param metingAPI Meting API 地址(可选)
|
|
71
78
|
* @returns 音频直链 URL
|
|
72
79
|
*/
|
|
73
|
-
async getAudioUrl(id: string
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
async getAudioUrl(id: string): Promise<string> {
|
|
81
|
+
const cookie = await this.#credentials.get('netease', 'cookie');
|
|
82
|
+
if (cookie) {
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(
|
|
85
|
+
`https://music.163.com/api/song/enhance/player/url?ids=[${id}]&br=320000`,
|
|
86
|
+
{
|
|
87
|
+
method: 'GET',
|
|
88
|
+
headers: { Cookie: cookie },
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
const result = await response.json() as {
|
|
92
|
+
data?: Array<{ url?: string | null }>;
|
|
93
|
+
};
|
|
94
|
+
const audioUrl = result.data?.[0]?.url;
|
|
95
|
+
if (audioUrl) return audioUrl;
|
|
96
|
+
} catch {
|
|
97
|
+
// cookie-based fetch failed, fall through to public API
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const apiUrl = 'https://api.injahow.cn/meting/';
|
|
101
|
+
const url = `${apiUrl}?type=url&id=${id}`;
|
|
102
|
+
const response = await fetch(url, { method: 'GET' });
|
|
103
|
+
const data = await response.json() as { url?: string; data?: { url?: string } };
|
|
104
|
+
const audioUrl = data.url ?? data.data?.url;
|
|
105
|
+
if (!audioUrl) throw new Error('Audio URL not found');
|
|
106
|
+
return audioUrl;
|
|
83
107
|
}
|
|
84
108
|
}
|
package/src/sources/qq.ts
CHANGED
|
@@ -1,148 +1,186 @@
|
|
|
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 type { CredentialStore } 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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
url.searchParams.set('format', 'json')
|
|
11
|
-
|
|
12
|
-
const response = await fetch(url, { method: 'GET' })
|
|
13
|
-
const data = await response.json() as { data: { song: { itemlist: MusicQQ[] } } }
|
|
14
|
-
|
|
15
|
-
const items = data.data?.song?.itemlist || []
|
|
16
|
-
return items.slice(0, limit).map(music => ({
|
|
17
|
-
id: music.id,
|
|
18
|
-
source: 'qq' as const,
|
|
19
|
-
title: music.name,
|
|
20
|
-
url: `https://y.qq.com/n/yqq/song/${music.mid}.html`
|
|
21
|
-
}))
|
|
22
|
-
} catch (error) {
|
|
23
|
-
console.error('QQ Music search failed:', error)
|
|
24
|
-
return []
|
|
25
|
-
}
|
|
18
|
+
readonly #credentials: CredentialStore;
|
|
19
|
+
|
|
20
|
+
constructor(credentials: CredentialStore) {
|
|
21
|
+
this.#credentials = credentials;
|
|
26
22
|
}
|
|
27
23
|
|
|
28
|
-
async
|
|
24
|
+
async search(keyword: string, limit = 10): Promise<MusicInfo[]> {
|
|
29
25
|
try {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
26
|
+
const cookie = await this.#credentials.get('qq', 'cookie');
|
|
27
|
+
const response = await fetch(MUSICU_API, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: getHeaders(cookie),
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
comm: { uin: '0', authst: '', ct: 29 },
|
|
32
|
+
search: {
|
|
33
|
+
method: 'DoSearchForQQMusicMobile',
|
|
34
|
+
module: 'music.search.SearchCgiService',
|
|
35
|
+
param: {
|
|
36
|
+
query: keyword,
|
|
37
|
+
grp: 1,
|
|
38
|
+
num_per_page: limit,
|
|
39
|
+
page_num: 1,
|
|
40
|
+
search_type: 0,
|
|
41
|
+
searchid: String(Date.now()),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const data = (await response.json()) as { code?: number; search?: { data?: { body?: { song?: { list?: any[] }; item_song?: any[] } } } };
|
|
48
|
+
if (data.code !== undefined && +data.code !== 0) return [];
|
|
49
|
+
|
|
50
|
+
const body = data.search?.data?.body ?? {};
|
|
51
|
+
const list = (body.song?.list ?? body.item_song ?? []) as any[];
|
|
52
|
+
|
|
53
|
+
return list.slice(0, limit).map((e) => {
|
|
54
|
+
const mid = e?.mid ?? '';
|
|
55
|
+
const albumMid = e?.album?.pmid ?? e?.album?.mid ?? '';
|
|
56
|
+
const singerMid = e?.singer?.[0]?.pmid ?? e?.singer?.[0]?.mid ?? '';
|
|
57
|
+
const cover = albumMid
|
|
58
|
+
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${albumMid}.jpg`
|
|
59
|
+
: singerMid
|
|
60
|
+
? `https://y.qq.com/music/photo_new/T001R300x300M000${singerMid}.jpg`
|
|
61
|
+
: '';
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
id: String(e.id),
|
|
65
|
+
source: 'qq' as const,
|
|
66
|
+
title: e.name,
|
|
67
|
+
artist: (e.singer ?? []).map((s: any) => s.name).join('/'),
|
|
68
|
+
album: e.album?.name,
|
|
69
|
+
url: `https://y.qq.com/n/ryqq/songDetail/${mid}`,
|
|
70
|
+
image: cover,
|
|
71
|
+
duration: e.interval,
|
|
72
|
+
_mid: mid,
|
|
73
|
+
_mediaMid: e.file?.media_mid,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
53
76
|
} catch (error) {
|
|
54
|
-
console.error('QQ Music
|
|
55
|
-
return
|
|
77
|
+
console.error('QQ Music search failed:', error);
|
|
78
|
+
return [];
|
|
56
79
|
}
|
|
57
80
|
}
|
|
58
81
|
|
|
59
82
|
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
|
-
|
|
83
|
+
const url = new URL(MUSICU_API);
|
|
84
|
+
url.searchParams.set('format', 'json');
|
|
85
|
+
url.searchParams.set('data', JSON.stringify({
|
|
86
|
+
comm: { ct: 24, cv: 0 },
|
|
87
|
+
songinfo: {
|
|
88
|
+
method: 'get_song_detail_yqq',
|
|
89
|
+
param: { song_type: 0, song_mid: '', song_id: parseInt(id) },
|
|
90
|
+
module: 'music.pf_song_detail_svr',
|
|
91
|
+
},
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
const response = await fetch(url, { method: 'GET' });
|
|
95
|
+
const result = await response.json() as any;
|
|
96
|
+
|
|
97
|
+
const trackInfo = result?.songinfo?.data?.track_info;
|
|
98
|
+
if (!trackInfo) throw new Error('Music not found');
|
|
99
|
+
|
|
100
|
+
const albumMid = trackInfo.album?.mid;
|
|
101
|
+
const image = albumMid
|
|
102
|
+
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${albumMid}.jpg`
|
|
103
|
+
: '';
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
id,
|
|
107
|
+
source: 'qq',
|
|
108
|
+
title: trackInfo.name,
|
|
109
|
+
url: `https://y.qq.com/n/ryqq/songDetail/${trackInfo.mid}`,
|
|
110
|
+
audio: await this.getAudioUrl(id, trackInfo.mid, trackInfo.file?.media_mid),
|
|
111
|
+
image,
|
|
112
|
+
duration: trackInfo.interval,
|
|
113
|
+
};
|
|
91
114
|
}
|
|
92
115
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
116
|
+
async getAudioUrl(id: string, mid?: string, mediaMid?: string): Promise<string> {
|
|
117
|
+
const cookie = await this.#credentials.get('qq', 'cookie');
|
|
118
|
+
if (cookie) {
|
|
119
|
+
try {
|
|
120
|
+
const uin = /uin=o?(\d+)/.exec(cookie)?.[1] ?? '0';
|
|
121
|
+
const songMid = mid ?? id;
|
|
122
|
+
const response = await fetch(MUSICU_API, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: getHeaders(cookie),
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
comm: {
|
|
127
|
+
ct: 19,
|
|
128
|
+
cv: '1891',
|
|
129
|
+
uin: '0',
|
|
130
|
+
guid: md5(`${uin}music`),
|
|
131
|
+
tmeAppID: 'qqmusic',
|
|
132
|
+
tmeLoginType: 2,
|
|
133
|
+
},
|
|
134
|
+
vkey: {
|
|
135
|
+
module: 'vkey.GetVkeyServer',
|
|
136
|
+
method: 'CgiGetVkey',
|
|
137
|
+
param: {
|
|
138
|
+
guid: md5(String(Date.now())),
|
|
139
|
+
songmid: [songMid],
|
|
140
|
+
songtype: [0],
|
|
141
|
+
uin: '0',
|
|
142
|
+
ctx: 1,
|
|
143
|
+
filename: [`C400${id}${mediaMid ?? songMid}.m4a`],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const result = await response.json() as any;
|
|
150
|
+
if (result.vkey && +result.vkey.code === 0) {
|
|
151
|
+
const purl = result.vkey.data?.midurlinfo?.[0]?.purl;
|
|
152
|
+
if (purl) return `https://ws.stream.qqmusic.qq.com/${purl}`;
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// fall through to public fallback
|
|
108
156
|
}
|
|
109
|
-
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 无 cookie 的公开 fallback:md5 签名直链
|
|
160
|
+
const songMid = mid ?? id;
|
|
161
|
+
const code = md5(`${songMid}q;z(&l~sdf2!nK`).substring(0, 5).toUpperCase();
|
|
162
|
+
return `https://c6.y.qq.com/rsc/fcgi-bin/fcg_pyq_play.fcg?songmid=${songMid}&code=${code}`;
|
|
110
163
|
}
|
|
111
164
|
|
|
112
|
-
/**
|
|
113
|
-
* 获取歌词
|
|
114
|
-
* @param id 音乐 ID
|
|
115
|
-
* @returns 歌词文本
|
|
116
|
-
*/
|
|
117
165
|
async getLyric(id: string): Promise<string | null> {
|
|
118
166
|
try {
|
|
119
|
-
const url = new URL('https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg')
|
|
120
|
-
url.searchParams.set('songmid', id)
|
|
121
|
-
url.searchParams.set('g_tk', '5381')
|
|
122
|
-
url.searchParams.set('format', 'json')
|
|
123
|
-
url.searchParams.set('inCharset', 'utf8')
|
|
124
|
-
url.searchParams.set('outCharset', 'utf-8')
|
|
125
|
-
url.searchParams.set('nobase64', '1')
|
|
126
|
-
|
|
127
|
-
const response = await fetch(url, {
|
|
167
|
+
const url = new URL('https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg');
|
|
168
|
+
url.searchParams.set('songmid', id);
|
|
169
|
+
url.searchParams.set('g_tk', '5381');
|
|
170
|
+
url.searchParams.set('format', 'json');
|
|
171
|
+
url.searchParams.set('inCharset', 'utf8');
|
|
172
|
+
url.searchParams.set('outCharset', 'utf-8');
|
|
173
|
+
url.searchParams.set('nobase64', '1');
|
|
174
|
+
|
|
175
|
+
const response = await fetch(url, {
|
|
128
176
|
method: 'GET',
|
|
129
|
-
headers: {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const data = await response.json() as { lyric?: string }
|
|
134
|
-
|
|
135
|
-
if (!data.lyric) return null
|
|
136
|
-
|
|
137
|
-
// QQ 音乐返回的歌词需要 base64 解码
|
|
138
|
-
try {
|
|
139
|
-
return Buffer.from(data.lyric, 'base64').toString('utf-8')
|
|
140
|
-
} catch {
|
|
141
|
-
return data.lyric
|
|
142
|
-
}
|
|
177
|
+
headers: { Referer: 'https://y.qq.com' },
|
|
178
|
+
});
|
|
179
|
+
const data = (await response.json()) as { lyric?: string };
|
|
180
|
+
return data.lyric ?? null;
|
|
143
181
|
} catch (error) {
|
|
144
|
-
console.error('QQ Music get lyric failed:', error)
|
|
145
|
-
return null
|
|
182
|
+
console.error('QQ Music get lyric failed:', error);
|
|
183
|
+
return null;
|
|
146
184
|
}
|
|
147
185
|
}
|
|
148
186
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,24 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
export type MusicSource = 'qq' | 'netease' | 'kuwo' | 'kugou';
|
|
2
2
|
|
|
3
|
-
/** 音乐源类型 */
|
|
4
|
-
export type MusicSource = "qq" | "netease";
|
|
5
|
-
|
|
6
|
-
/** 音乐分享内容 */
|
|
7
3
|
export interface MusicInfo {
|
|
8
4
|
id: string;
|
|
9
5
|
source: MusicSource;
|
|
10
|
-
/** 跳转链接 */
|
|
11
6
|
url: string;
|
|
12
|
-
/** 音乐标题 */
|
|
13
7
|
title: string;
|
|
8
|
+
artist?: string;
|
|
9
|
+
album?: string;
|
|
10
|
+
duration?: number;
|
|
14
11
|
}
|
|
12
|
+
|
|
15
13
|
export interface MusicDetail extends MusicInfo {
|
|
16
14
|
image: string;
|
|
17
|
-
audio:string;
|
|
18
|
-
duration?: number;
|
|
15
|
+
audio: string;
|
|
19
16
|
}
|
|
20
17
|
|
|
21
|
-
/** 音乐源配置 */
|
|
22
18
|
export interface MusicSourceConfig {
|
|
23
19
|
appid: number;
|
|
24
20
|
package: string;
|
|
@@ -27,21 +23,6 @@ export interface MusicSourceConfig {
|
|
|
27
23
|
version: string;
|
|
28
24
|
}
|
|
29
25
|
|
|
30
|
-
/** QQ 音乐 API 响应 */
|
|
31
|
-
export interface MusicQQ {
|
|
32
|
-
id: string;
|
|
33
|
-
mid: string;
|
|
34
|
-
name: string;
|
|
35
|
-
docid: string;
|
|
36
|
-
singer: string;
|
|
37
|
-
album?: {
|
|
38
|
-
mid: string;
|
|
39
|
-
name: string;
|
|
40
|
-
};
|
|
41
|
-
interval?: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** 网易云音乐 API 响应 */
|
|
45
26
|
export interface Music163 {
|
|
46
27
|
id: string;
|
|
47
28
|
name: string;
|
|
@@ -58,10 +39,7 @@ export interface Music163 {
|
|
|
58
39
|
};
|
|
59
40
|
}
|
|
60
41
|
|
|
61
|
-
/** 音乐搜索服务接口 */
|
|
62
42
|
export interface MusicSearchService {
|
|
63
|
-
/** 搜索音乐 */
|
|
64
43
|
search(keyword: string, limit?: number): Promise<MusicInfo[]>;
|
|
65
|
-
/** 获取音乐详情 */
|
|
66
44
|
getDetail(id: string): Promise<MusicDetail>;
|
|
67
45
|
}
|
package/lib/async-jsx.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { SendContent } from "zhin.js";
|
|
2
|
-
/**
|
|
3
|
-
* 异步组件包装器
|
|
4
|
-
* 类似 Next.js 的异步组件支持
|
|
5
|
-
*/
|
|
6
|
-
export declare function AsyncComponent<P = any>(Component: (props: P) => Promise<SendContent>): (props: P) => SendContent;
|
|
7
|
-
/**
|
|
8
|
-
* 渲染异步内容
|
|
9
|
-
* 检查是否是异步标记,如果是则执行
|
|
10
|
-
*/
|
|
11
|
-
export declare function renderAsync(content: any): Promise<SendContent>;
|
|
12
|
-
/**
|
|
13
|
-
* Suspense 组件工厂
|
|
14
|
-
* 用于包装异步组件并提供 fallback
|
|
15
|
-
*/
|
|
16
|
-
export declare function createSuspense(fallback?: string): (props: {
|
|
17
|
-
children: any;
|
|
18
|
-
}) => Promise<SendContent>;
|
|
19
|
-
//# sourceMappingURL=async-jsx.d.ts.map
|
package/lib/async-jsx.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"async-jsx.d.ts","sourceRoot":"","sources":["../src/async-jsx.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtC;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,GAAG,GAAG,EACpC,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,GAC5C,CAAC,KAAK,EAAE,CAAC,KAAK,WAAW,CAc3B;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,CAQpE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,GAAE,MAAiB,IACzB,OAAO;IAAE,QAAQ,EAAE,GAAG,CAAA;CAAE,KAAG,OAAO,CAAC,WAAW,CAAC,CAQ/E"}
|