@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.
Files changed (111) hide show
  1. package/README.md +28 -27
  2. package/commands/cookie/delete/[source]/index.js +23 -0
  3. package/commands/cookie/delete/[source]/index.ts +27 -0
  4. package/commands/cookie/get/[source]/index.js +29 -0
  5. package/commands/cookie/get/[source]/index.ts +33 -0
  6. package/commands/cookie/set/[source]/index.js +23 -0
  7. package/commands/cookie/set/[source]/index.ts +27 -0
  8. package/commands/cookie/status/index.js +25 -0
  9. package/commands/cookie/status/index.ts +25 -0
  10. package/commands/login/[platform]/index.js +66 -0
  11. package/commands/login/[platform]/index.ts +74 -0
  12. package/commands//347/202/271/346/255/214/[keyword]/index.js +63 -0
  13. package/commands//347/202/271/346/255/214/[keyword]/index.ts +75 -0
  14. package/components/share-music/index.js +24 -0
  15. package/components/share-music/index.ts +29 -0
  16. package/lib/config.d.ts +3 -3
  17. package/lib/config.d.ts.map +1 -1
  18. package/lib/config.js +36 -7
  19. package/lib/config.js.map +1 -1
  20. package/lib/credential-store.d.ts +37 -0
  21. package/lib/credential-store.d.ts.map +1 -0
  22. package/lib/credential-store.js +85 -0
  23. package/lib/credential-store.js.map +1 -0
  24. package/lib/index.d.ts +8 -1
  25. package/lib/index.d.ts.map +1 -1
  26. package/lib/index.js +7 -112
  27. package/lib/index.js.map +1 -1
  28. package/lib/login/index.d.ts +27 -0
  29. package/lib/login/index.d.ts.map +1 -0
  30. package/lib/login/index.js +124 -0
  31. package/lib/login/index.js.map +1 -0
  32. package/lib/login/netease.d.ts +6 -0
  33. package/lib/login/netease.d.ts.map +1 -0
  34. package/lib/login/netease.js +58 -0
  35. package/lib/login/netease.js.map +1 -0
  36. package/lib/login/qq.d.ts +7 -0
  37. package/lib/login/qq.d.ts.map +1 -0
  38. package/lib/login/qq.js +139 -0
  39. package/lib/login/qq.js.map +1 -0
  40. package/lib/login/types.d.ts +19 -0
  41. package/lib/login/types.d.ts.map +1 -0
  42. package/lib/login/types.js +11 -0
  43. package/lib/login/types.js.map +1 -0
  44. package/lib/music-lib.d.ts +32 -0
  45. package/lib/music-lib.d.ts.map +1 -0
  46. package/lib/music-lib.js +58 -0
  47. package/lib/music-lib.js.map +1 -0
  48. package/lib/runtime.d.ts +12 -0
  49. package/lib/runtime.d.ts.map +1 -0
  50. package/lib/runtime.js +3 -0
  51. package/lib/runtime.js.map +1 -0
  52. package/lib/session.d.ts +35 -0
  53. package/lib/session.d.ts.map +1 -0
  54. package/lib/session.js +51 -0
  55. package/lib/session.js.map +1 -0
  56. package/lib/sources/index.d.ts +4 -2
  57. package/lib/sources/index.d.ts.map +1 -1
  58. package/lib/sources/index.js +12 -6
  59. package/lib/sources/index.js.map +1 -1
  60. package/lib/sources/kugou.d.ts +6 -0
  61. package/lib/sources/kugou.d.ts.map +1 -0
  62. package/lib/sources/kugou.js +69 -0
  63. package/lib/sources/kugou.js.map +1 -0
  64. package/lib/sources/kuwo.d.ts +7 -0
  65. package/lib/sources/kuwo.d.ts.map +1 -0
  66. package/lib/sources/kuwo.js +69 -0
  67. package/lib/sources/kuwo.js.map +1 -0
  68. package/lib/sources/netease.d.ts +4 -1
  69. package/lib/sources/netease.d.ts.map +1 -1
  70. package/lib/sources/netease.js +25 -6
  71. package/lib/sources/netease.js.map +1 -1
  72. package/lib/sources/qq.d.ts +4 -14
  73. package/lib/sources/qq.d.ts.map +1 -1
  74. package/lib/sources/qq.js +117 -82
  75. package/lib/sources/qq.js.map +1 -1
  76. package/lib/types.d.ts +4 -24
  77. package/lib/types.d.ts.map +1 -1
  78. package/lib/types.js +0 -1
  79. package/lib/types.js.map +1 -1
  80. package/middlewares/music-selection/index.js +48 -0
  81. package/middlewares/music-selection/index.ts +62 -0
  82. package/package.json +66 -12
  83. package/plugin.js +45 -0
  84. package/schema.json +6 -0
  85. package/skills/music/SKILL.md +2 -2
  86. package/skills/music/tools/music-search/index.js +22 -0
  87. package/skills/music/tools/music-search/index.ts +33 -0
  88. package/skills/music/tools/music-share/index.js +21 -0
  89. package/skills/music/tools/music-share/index.ts +26 -0
  90. package/src/config.ts +54 -30
  91. package/src/credential-store.ts +125 -0
  92. package/src/index.ts +37 -0
  93. package/src/login/index.ts +154 -0
  94. package/src/login/netease.ts +69 -0
  95. package/src/login/qq.ts +154 -0
  96. package/src/login/types.ts +27 -0
  97. package/src/music-lib.ts +71 -0
  98. package/src/runtime.ts +17 -0
  99. package/src/session.ts +77 -0
  100. package/src/sources/index.ts +17 -10
  101. package/src/sources/kugou.ts +92 -0
  102. package/src/sources/kuwo.ts +94 -0
  103. package/src/sources/netease.ts +35 -11
  104. package/src/sources/qq.ts +164 -126
  105. package/src/types.ts +6 -28
  106. package/lib/async-jsx.d.ts +0 -19
  107. package/lib/async-jsx.d.ts.map +0 -1
  108. package/lib/async-jsx.js +0 -48
  109. package/lib/async-jsx.js.map +0 -1
  110. package/src/async-jsx.ts +0 -54
  111. 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(/&nbsp;/g, ' ') ?? '',
38
+ artist: item.artist?.replace(/&nbsp;/g, ' '),
39
+ album: item.album?.replace(/&nbsp;/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
+ }
@@ -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, metingAPI?: string): Promise<string> {
74
- // 默认使用 Meting API
75
- const apiUrl = metingAPI || 'https://api.injahow.cn/meting/'
76
- const url = `${apiUrl}?type=url&id=${id}`
77
-
78
- const response = await fetch(url, { method: 'GET' })
79
- const data = await response.json() as { url?: string, data?: { url?: string } }
80
- if(!data.url && !data.data?.url) throw new Error('Audio URL not found')
81
- // 不同的 Meting API 返回格式可能不同
82
- return data.url || data.data?.url!
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
- // plugins/utils/music/src/sources/qq.ts
2
- import type { MusicSearchService, MusicDetail,MusicInfo, MusicQQ } from '../types.js'
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
- async search(keyword: string, limit = 10): Promise<MusicInfo[]> {
7
- try {
8
- const url = new URL('https://c.y.qq.com/splcloud/fcgi-bin/smartbox_new.fcg')
9
- url.searchParams.set('key', keyword)
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 getCover(id: string): Promise<string | null> {
24
+ async search(keyword: string, limit = 10): Promise<MusicInfo[]> {
29
25
  try {
30
- const url = new URL('https://u.y.qq.com/cgi-bin/musicu.fcg')
31
- url.searchParams.set('format', 'json')
32
- url.searchParams.set('inCharset', 'utf8')
33
- url.searchParams.set('outCharset', 'utf-8')
34
- url.searchParams.set('notice', '0')
35
- url.searchParams.set('platform', 'yqq.json')
36
- url.searchParams.set('needNewCode', '0')
37
- url.searchParams.set('data', JSON.stringify({
38
- comm: { ct: 24, cv: 0 },
39
- songinfo: {
40
- method: 'get_song_detail_yqq',
41
- param: { song_type: 0, song_mid: '', song_id: parseInt(id) },
42
- module: 'music.pf_song_detail_svr'
43
- }
44
- }))
45
-
46
- const response = await fetch(url, { method: 'GET' })
47
- const result = await response.json()
48
-
49
- const albumMid = result?.songinfo?.data?.track_info?.album?.mid
50
- if (!albumMid) return null
51
-
52
- return `https://y.gtimg.cn/music/photo_new/T002R300x300M000${albumMid}.jpg`
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 get cover failed:', error)
55
- return null
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('https://u.y.qq.com/cgi-bin/musicu.fcg')
61
- url.searchParams.set('format', 'json')
62
- url.searchParams.set('data', JSON.stringify({
63
- comm: { ct: 24, cv: 0 },
64
- songinfo: {
65
- method: 'get_song_detail_yqq',
66
- param: { song_type: 0, song_mid: '', song_id: parseInt(id) },
67
- module: 'music.pf_song_detail_svr'
68
- }
69
- }))
70
-
71
- const response = await fetch(url, { method: 'GET' })
72
- const result = await response.json()
73
-
74
- const trackInfo = result?.songinfo?.data?.track_info
75
- if (!trackInfo) throw new Error('Music not found')
76
-
77
- const albumMid = trackInfo.album?.mid
78
- const image = albumMid
79
- ? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${albumMid}.jpg`
80
- : undefined
81
-
82
- return {
83
- id,
84
- source: 'qq',
85
- title: trackInfo.name,
86
- url: `https://y.qq.com/n/yqq/song/${trackInfo.mid}.html`,
87
- audio: await this.getAudioUrl(id),
88
- image: image || '',
89
- duration: trackInfo.interval,
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
- * 获取音频直链(需要第三方 API)
95
- * @param id 音乐 ID
96
- * @param metingAPI Meting API 地址(可选)
97
- * @returns 音频直链 URL
98
- */
99
- async getAudioUrl(id: string, metingAPI?: string): Promise<string> {
100
- // QQ 音乐需要使用第三方 API
101
- const apiUrl = metingAPI || 'https://api.injahow.cn/meting/'
102
- const url = `${apiUrl}?type=url&id=${id}&source=qq`
103
-
104
- const response = await fetch(url, { method: 'GET' })
105
- const data = await response.json() as { url?: string, data?: { url?: string } }
106
- if(!data.url && !data.data?.url) {
107
- throw new Error('Audio URL not found')
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
- return data.url || data.data?.url!
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
- 'Referer': 'https://y.qq.com'
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
- // plugins/utils/music/src/types.ts
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
  }
@@ -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
@@ -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"}