mioku-plugin-music 2.1.0 → 2.3.0

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/config.md CHANGED
@@ -15,6 +15,8 @@ fields:
15
15
  options:
16
16
  - value: applemusic
17
17
  label: Apple Music
18
+ - value: netease
19
+ label: NetEase Cloud Music
18
20
 
19
21
  - key: base.applemusic.storefront
20
22
  label: Apple Music Storefront
@@ -33,6 +35,26 @@ fields:
33
35
  type: secret
34
36
  description: Apple Music 下载高质量 AAC 必需 token,无token仅能下载30s音频
35
37
  placeholder: eyJ...
38
+
39
+ - key: base.netease.quality
40
+ label: NetEase 音质
41
+ type: select
42
+ description: 下载音质档位,lossless/hires 需要 VIP cookie
43
+ options:
44
+ - value: standard
45
+ label: 标准 (128k)
46
+ - value: exhigh
47
+ label: 极高 (320k,推荐)
48
+ - value: lossless
49
+ label: 无损 FLAC
50
+ - value: hires
51
+ label: Hi-Res
52
+
53
+ - key: base.netease.defaultCookie
54
+ label: NetEase Cookie
55
+ type: secret
56
+ description: 网易云 MUSIC_U cookie,无 cookie 仅能搜索;VIP 音质必须配置
57
+ placeholder: MUSIC_U=...; os=pc; appver=8.9.75;
36
58
  ---
37
59
 
38
60
  ```mioku-fields
@@ -41,9 +63,19 @@ keys:
41
63
  - base.defaultProvider
42
64
  ```
43
65
 
66
+ ### 以下为 Apple Music 的配置区域
67
+
44
68
  ```mioku-fields
45
69
  keys:
46
70
  - base.applemusic.storefront
47
71
  - base.applemusic.language
48
72
  - base.applemusic.defaultMediaUserToken
49
73
  ```
74
+
75
+ ### 以下为网易云音乐的配置区域
76
+
77
+ ```mioku-fields
78
+ keys:
79
+ - base.netease.quality
80
+ - base.netease.defaultCookie
81
+ ```
package/config.ts CHANGED
@@ -8,4 +8,8 @@ export const MUSIC_DEFAULTS: MusicBaseConfig = {
8
8
  language: "zh-CN",
9
9
  defaultMediaUserToken: "",
10
10
  },
11
+ netease: {
12
+ quality: "exhigh",
13
+ defaultCookie: "",
14
+ },
11
15
  };
package/index.ts CHANGED
@@ -3,6 +3,7 @@ import type { AIService } from "mioku";
3
3
  import type { ScreenshotService } from "mioku";
4
4
  import type { ConfigService } from "mioku";
5
5
  import type { AppleMusicServiceApi } from "mioku-service-applemusic";
6
+ import type { NeteaseServiceApi } from "mioku-service-netease";
6
7
  import { resetMusicRuntimeState, setMusicRuntimeState } from "./runtime";
7
8
  import { MusicPluginRuntime } from "./runtime-core/service";
8
9
  import { MUSIC_DEFAULTS } from "./config";
@@ -25,6 +26,9 @@ export default definePlugin({
25
26
  const applemusicService = ctx.services?.applemusic as
26
27
  | AppleMusicServiceApi
27
28
  | undefined;
29
+ const neteaseService = ctx.services?.netease as
30
+ | NeteaseServiceApi
31
+ | undefined;
28
32
  let baseConfig = cloneConfig(MUSIC_DEFAULTS);
29
33
 
30
34
  if (configService) {
@@ -42,6 +46,7 @@ export default definePlugin({
42
46
  aiService,
43
47
  screenshotService,
44
48
  applemusicService,
49
+ neteaseService,
45
50
  });
46
51
  runtime.updateConfig(baseConfig);
47
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-music",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "音乐插件:点歌、搜索音乐、听歌语音发送",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -17,6 +17,20 @@
17
17
  "ai",
18
18
  "screenshot"
19
19
  ],
20
+ "accessHooks": [
21
+ {
22
+ "id": "点歌",
23
+ "match": "/^\\/?点歌\\s+/"
24
+ },
25
+ {
26
+ "id": "听",
27
+ "match": "/^听\\d+$/"
28
+ },
29
+ {
30
+ "id": "原曲",
31
+ "match": "/^原曲\\d+$/"
32
+ }
33
+ ],
20
34
  "help": {
21
35
  "title": "音乐",
22
36
  "description": "点歌并发送语音,支持搜索结果图片列表",
@@ -45,11 +59,13 @@
45
59
  "peerDependencies": {
46
60
  "mioku": "^0.8.0",
47
61
  "mioki": "^0.16.0",
48
- "mioku-service-applemusic": "^2.0.0"
62
+ "mioku-service-applemusic": "^2.0.0",
63
+ "mioku-service-netease": "^1.0.0"
49
64
  },
50
65
  "devDependencies": {
51
66
  "mioku": "workspace:*",
52
67
  "mioki": "^0.16.0",
53
- "mioku-service-applemusic": "workspace:*"
68
+ "mioku-service-applemusic": "workspace:*",
69
+ "mioku-service-netease": "workspace:*"
54
70
  }
55
71
  }
@@ -1,5 +1,7 @@
1
1
  import type { AppleMusicServiceApi } from "mioku-service-applemusic";
2
+ import type { NeteaseServiceApi } from "mioku-service-netease";
2
3
  import { AppleMusicProvider } from "./applemusic-provider";
4
+ import { NeteaseProvider } from "./netease-provider";
3
5
  import {
4
6
  type MusicProvider,
5
7
  type MusicProviderClientOptions,
@@ -8,6 +10,7 @@ import {
8
10
 
9
11
  export interface MusicProviderFactoryOptions {
10
12
  applemusic?: AppleMusicServiceApi;
13
+ netease?: NeteaseServiceApi;
11
14
  }
12
15
 
13
16
  interface MusicProviderRegistryItem {
@@ -32,6 +35,20 @@ const MUSIC_PROVIDER_REGISTRY: MusicProviderRegistryItem[] = [
32
35
  return new AppleMusicProvider(services.applemusic, clientOptions);
33
36
  },
34
37
  },
38
+ {
39
+ name: "netease",
40
+ serviceName: "netease",
41
+ isAvailable: (services) => Boolean(services.netease),
42
+ create: (services, clientOptions) => {
43
+ if (!services.netease) {
44
+ throw new Error("netease 服务未加载");
45
+ }
46
+ return new NeteaseProvider(services.netease, {
47
+ neteaseCookie: clientOptions?.neteaseCookie,
48
+ neteaseQuality: clientOptions?.neteaseQuality,
49
+ });
50
+ },
51
+ },
35
52
  ];
36
53
 
37
54
  function getProviderRegistryItem(
@@ -0,0 +1,88 @@
1
+ import type { NeteaseServiceApi } from "mioku-service-netease";
2
+ import type {
3
+ DownloadSongResult,
4
+ MusicAlbumDetail,
5
+ MusicProvider,
6
+ MusicProviderClientOptions,
7
+ MusicProviderName,
8
+ MusicSearchResult,
9
+ MusicSongDetail,
10
+ } from "../types";
11
+
12
+ export class NeteaseProvider implements MusicProvider {
13
+ readonly name: MusicProviderName = "netease";
14
+ private readonly client: ReturnType<NeteaseServiceApi["createClient"]>;
15
+
16
+ constructor(api: NeteaseServiceApi, options?: MusicProviderClientOptions) {
17
+ this.client = api.createClient({
18
+ cookie: options?.neteaseCookie,
19
+ quality: options?.neteaseQuality,
20
+ });
21
+ }
22
+
23
+ async searchSongs(query: string, limit: number = 15): Promise<MusicSearchResult> {
24
+ const result = await this.client.searchSongs({ query, limit });
25
+ return {
26
+ query,
27
+ provider: this.name,
28
+ tracks: result.songs.map((item) => ({
29
+ id: item.id,
30
+ provider: this.name,
31
+ title: item.name,
32
+ artist: item.artistName,
33
+ album: item.albumName,
34
+ coverUrl: item.artworkUrl,
35
+ durationMs: item.durationInMillis,
36
+ })),
37
+ };
38
+ }
39
+
40
+ async getSongDetail(songId: string): Promise<MusicSongDetail> {
41
+ const detail = await this.client.getSongDetail({ songId });
42
+ return {
43
+ id: detail.id,
44
+ provider: this.name,
45
+ title: detail.name,
46
+ artist: detail.artistName,
47
+ album: detail.albumName,
48
+ coverUrl: detail.artworkUrl,
49
+ releaseDate: detail.releaseDate,
50
+ durationMs: detail.durationInMillis,
51
+ };
52
+ }
53
+
54
+ async getAlbumDetail(albumId: string): Promise<MusicAlbumDetail> {
55
+ const detail = await this.client.getAlbumDetail({ albumId });
56
+ return {
57
+ id: detail.id,
58
+ provider: this.name,
59
+ title: detail.name,
60
+ artist: detail.artistName,
61
+ coverUrl: detail.artworkUrl,
62
+ releaseDate: detail.releaseDate,
63
+ tracks: detail.tracks.map((track) => ({
64
+ id: track.id,
65
+ title: track.name,
66
+ artist: track.artistName,
67
+ durationMs: track.durationInMillis,
68
+ })),
69
+ };
70
+ }
71
+
72
+ async downloadSong(songId: string): Promise<DownloadSongResult> {
73
+ const result = await this.client.downloadSongAac({ songId });
74
+ return {
75
+ filePath: result.filePath,
76
+ sourceType: result.sourceType,
77
+ };
78
+ }
79
+
80
+ async downloadCover(coverUrl: string, fileName?: string): Promise<string> {
81
+ const result = await this.client.downloadCover({
82
+ artworkUrl: coverUrl,
83
+ fileName: fileName || "cover",
84
+ size: 1200,
85
+ });
86
+ return result.filePath;
87
+ }
88
+ }
@@ -2,6 +2,7 @@ import type { MusicProviderName } from "../types";
2
2
 
3
3
  export const MUSIC_PROVIDER_LABELS: Record<MusicProviderName, string> = {
4
4
  applemusic: "Apple Music",
5
+ netease: "NetEase Cloud Music",
5
6
  };
6
7
 
7
8
  export function getMusicProviderLabel(provider: MusicProviderName | string): string {
@@ -1,6 +1,7 @@
1
1
  import type { AIService } from "mioku";
2
2
  import type { ScreenshotService } from "mioku";
3
3
  import type { AppleMusicServiceApi } from "mioku-service-applemusic";
4
+ import type { NeteaseServiceApi } from "mioku-service-netease";
4
5
  import { MUSIC_DEFAULTS } from "../config";
5
6
  import {
6
7
  createMusicProvider,
@@ -28,6 +29,7 @@ interface MusicPluginRuntimeDeps {
28
29
  aiService?: AIService;
29
30
  screenshotService?: ScreenshotService;
30
31
  applemusicService?: AppleMusicServiceApi;
32
+ neteaseService?: NeteaseServiceApi;
31
33
  }
32
34
 
33
35
  function parseListenIndex(text: string): number | null {
@@ -193,8 +195,8 @@ export class MusicPluginRuntime {
193
195
  if ("tracks" in result && Array.isArray((result as any).tracks)) {
194
196
  this.deps.logger.warn(`[music] downloadSong 返回了 tracks 字段`);
195
197
  }
196
- if (result.sourceType !== "hls") {
197
- throw new Error("当前未获取到高质量 HLS 音频,请检查 media user token");
198
+ if (result.sourceType !== "hls" && result.sourceType !== "file") {
199
+ throw new Error("当前未获取到高质量音频,请检查 provider 配置");
198
200
  }
199
201
  const isPrivate = event?.message_type !== "group";
200
202
  const sendAsFile = isPrivate || options?.forceFile === true;
@@ -350,14 +352,19 @@ export class MusicPluginRuntime {
350
352
  String(mediaUserToken || "").trim() ||
351
353
  String(this.config.applemusic.defaultMediaUserToken || "").trim() ||
352
354
  undefined;
355
+ const neteaseCookie =
356
+ String(this.config.netease?.defaultCookie || "").trim() || undefined;
353
357
 
354
358
  return createMusicProvider(
355
359
  resolvedProvider,
356
360
  {
357
361
  applemusic: this.deps.applemusicService,
362
+ netease: this.deps.neteaseService,
358
363
  },
359
364
  {
360
365
  mediaUserToken: finalMediaUserToken,
366
+ neteaseCookie,
367
+ neteaseQuality: this.config.netease?.quality,
361
368
  storefront: this.config.applemusic.storefront,
362
369
  language: this.config.applemusic.language,
363
370
  },
@@ -389,6 +396,7 @@ export class MusicPluginRuntime {
389
396
  private getProviderServices() {
390
397
  return {
391
398
  applemusic: this.deps.applemusicService,
399
+ netease: this.deps.neteaseService,
392
400
  };
393
401
  }
394
402
 
package/types.ts CHANGED
@@ -1,6 +1,8 @@
1
- export type MusicProviderName = "applemusic";
1
+ export type MusicProviderName = "applemusic" | "netease";
2
2
 
3
- export const MUSIC_PROVIDER_NAMES: MusicProviderName[] = ["applemusic"];
3
+ export const MUSIC_PROVIDER_NAMES: MusicProviderName[] = ["applemusic", "netease"];
4
+
5
+ export type MusicProviderQuality = "standard" | "exhigh" | "lossless" | "hires";
4
6
 
5
7
  export interface MusicBaseConfig {
6
8
  searchLimit: number;
@@ -10,6 +12,10 @@ export interface MusicBaseConfig {
10
12
  language: string;
11
13
  defaultMediaUserToken?: string;
12
14
  };
15
+ netease: {
16
+ quality: MusicProviderQuality;
17
+ defaultCookie: string;
18
+ };
13
19
  }
14
20
 
15
21
  export interface MusicTrack {
@@ -59,13 +65,15 @@ export interface MusicSearchResult {
59
65
 
60
66
  export interface DownloadSongResult {
61
67
  filePath: string;
62
- sourceType: "hls" | "preview";
68
+ sourceType: "hls" | "preview" | "file";
63
69
  }
64
70
 
65
71
  export interface MusicProviderClientOptions {
66
72
  mediaUserToken?: string;
67
73
  storefront?: string;
68
74
  language?: string;
75
+ neteaseCookie?: string;
76
+ neteaseQuality?: MusicProviderQuality;
69
77
  }
70
78
 
71
79
  export interface MusicProvider {