@lumia.wang/koishi-plugin-adapter-bilibili 0.1.1 → 0.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.
@@ -5,6 +5,7 @@ Other plugins can use these methods on an authenticated `bot.internal` without m
5
5
  | Method | Result |
6
6
  | --- | --- |
7
7
  | `getVideoSubtitle(bvid)` | `{ bvid, cid, language, text }`; `text` is empty when no subtitle track exists |
8
+ | `getVideoAudio(bvid, { maxDuration?, maxBytes? })` | Since 0.1.2: `{ bvid, cid, duration, mime: 'audio/mp4', data: Buffer }` for the first part's AAC track |
8
9
  | `getVideoComments(bvid, options?)` | Normalized comments with string IDs, text, author, likes and mention presence |
9
10
  | `getUserVideos(uid, { days?, pages? })` | Recent submissions, ordered by publication time |
10
11
  | `getRelatedVideos(bvid)` | Related videos as `VideoBrief[]` |
@@ -15,6 +16,7 @@ Other plugins can use these methods on an authenticated `bot.internal` without m
15
16
 
16
17
  `getVideoComments` accepts `limit` (1–60), `root` (thread root ID), `sort` (`hot` or `time`), and `anonymous`.
17
18
  `getUserVideos` accepts `days` (1–90, default 7) and `pages` (1–5, default 2).
19
+ `getVideoAudio` defaults to 600 seconds and 32 MiB. Callers may choose smaller limits, up to 1800 seconds and 64 MiB. Invalid limits, excessive duration, an unavailable AAC track, empty downloads and excessive download size throw errors. Interrupted/oversized streams are closed. Audio downloads use the context HTTP client without the adapter Cookie, while playback URL requests reuse the account and WBI signature. No external downloader or media process is started.
18
20
  Mention objects are `{ id: 'numeric UID', name: 'exact nickname' }`; include the matching `@nickname` in the text. IDs should be passed as strings.
19
21
 
20
22
  These new APIs propagate request failures and Bilibili error codes instead of treating them as empty content. A missing comment receipt does not trigger a retry. Existing `sendComment` behavior is unchanged: it remains a reply-only API for received comment targets.
@@ -45,7 +45,19 @@ export interface FavoriteFolder {
45
45
  id: number;
46
46
  title: string;
47
47
  }
48
+ export interface VideoAudioOptions {
49
+ maxDuration?: number;
50
+ maxBytes?: number;
51
+ }
52
+ export interface VideoAudio {
53
+ bvid: string;
54
+ cid: number;
55
+ duration: number;
56
+ mime: 'audio/mp4';
57
+ data: Buffer;
58
+ }
48
59
  export interface VideoToolkit {
60
+ getVideoAudio(bvid: string, options?: VideoAudioOptions): Promise<VideoAudio>;
49
61
  getVideoSubtitle(bvid: string): Promise<VideoSubtitle>;
50
62
  getVideoComments(bvid: string, options?: VideoCommentOptions): Promise<VideoComment[]>;
51
63
  getUserVideos(uid: string, options?: UserVideoOptions): Promise<VideoBrief[]>;
@@ -64,6 +76,8 @@ export declare class VideoToolkitAPI implements VideoToolkit {
64
76
  private post;
65
77
  private info;
66
78
  getVideoSubtitle(bvid: string): Promise<VideoSubtitle>;
79
+ /** Downloads one bounded AAC track for Node-based audio processing. */
80
+ getVideoAudio(bvid: string, options?: VideoAudioOptions): Promise<VideoAudio>;
67
81
  getVideoComments(bvid: string, options?: VideoCommentOptions): Promise<VideoComment[]>;
68
82
  getUserVideos(uid: string, options?: UserVideoOptions): Promise<VideoBrief[]>;
69
83
  getRelatedVideos(bvid: string): Promise<VideoBrief[]>;
@@ -1,6 +1,6 @@
1
1
  import { Context } from 'koishi';
2
2
  import { BilibiliDmBot } from '../bot/bot';
3
- import { VideoCommentOptions, UserVideoOptions } from './apis/videoToolkit';
3
+ import { VideoCommentOptions, UserVideoOptions, VideoAudioOptions } from './apis/videoToolkit';
4
4
  import { BilibiliCommentMention, BilibiliCommentTarget } from './apis/comment';
5
5
  import { DynamicItem, FollowingUser, InternalInterface, SearchUser, SearchVideo, SearchLiveRoom, SearchLiveUser, SearchArticle, ComprehensiveSearchResponse, SearchOptions, VideoData, ExternalParseResponse, VideoConclusionData } from './apis/types';
6
6
  export declare class Internal implements InternalInterface {
@@ -21,6 +21,7 @@ export declare class Internal implements InternalInterface {
21
21
  isCommentPollingActive(): boolean;
22
22
  sendComment(channelId: string, content: string, target?: BilibiliCommentTarget, mentions?: BilibiliCommentMention[]): Promise<string | null>;
23
23
  getVideoSubtitle(bvid: string): Promise<import(".").VideoSubtitle>;
24
+ getVideoAudio(bvid: string, options?: VideoAudioOptions): Promise<import(".").VideoAudio>;
24
25
  getVideoComments(bvid: string, options?: VideoCommentOptions): Promise<import(".").VideoComment[]>;
25
26
  getUserVideos(uid: string, options?: UserVideoOptions): Promise<import(".").VideoBrief[]>;
26
27
  getRelatedVideos(bvid: string): Promise<import(".").VideoBrief[]>;
package/lib/index.js CHANGED
@@ -2668,6 +2668,41 @@ var init_videoToolkit = __esm({
2668
2668
  const body = await this.ctx.http.get(url2.href);
2669
2669
  return { bvid, cid, language: track.lan, text: body.body.map((row) => row.content).join("\n") };
2670
2670
  }
2671
+ /** Downloads one bounded AAC track for Node-based audio processing. */
2672
+ async getVideoAudio(bvid, options = {}) {
2673
+ const maxDuration = options.maxDuration ?? 600;
2674
+ const maxBytes = options.maxBytes ?? 32 * 1024 * 1024;
2675
+ if (!Number.isFinite(maxDuration) || maxDuration <= 0 || maxDuration > 1800) throw new Error("Invalid audio duration limit");
2676
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0 || maxBytes > 64 * 1024 * 1024) throw new Error("Invalid audio size limit");
2677
+ const info = await this.info(bvid);
2678
+ const cid = info.cid || info.pages?.[0]?.cid;
2679
+ if (!cid) throw new Error("Video has no cid");
2680
+ if (!info.duration || info.duration > maxDuration) throw new Error(`Video exceeds audio duration limit (${maxDuration}s)`);
2681
+ const player = await this.get(
2682
+ "/x/player/wbi/playurl",
2683
+ { bvid, cid, fnval: 16, fnver: 0, fourk: 0, qn: 16 },
2684
+ true
2685
+ );
2686
+ const tracks = (player.dash?.audio || []).filter((track2) => [30216, 30232, 30280].includes(track2.id) && (!track2.codecid || track2.codecid === 0));
2687
+ const track = tracks.sort((a, b) => (a.bandwidth || 0) - (b.bandwidth || 0))[0];
2688
+ if (!track) throw new Error("Video has no supported AAC audio track");
2689
+ const url2 = new URL(track.baseUrl || track.base_url || "");
2690
+ if (url2.protocol !== "https:") throw new Error("Invalid audio URL protocol");
2691
+ const stream4 = await this.ctx.http.get(url2.href, {
2692
+ responseType: "stream",
2693
+ timeout: 6e4,
2694
+ headers: { Referer: `https://www.bilibili.com/video/${bvid}`, "User-Agent": "Mozilla/5.0" }
2695
+ });
2696
+ const chunks = [];
2697
+ let length = 0;
2698
+ for await (const chunk of stream4) {
2699
+ length += chunk.byteLength;
2700
+ if (length > maxBytes) throw new Error(`Audio exceeds download size limit (${maxBytes} bytes)`);
2701
+ chunks.push(Buffer.from(chunk));
2702
+ }
2703
+ if (!length) throw new Error("Video audio is empty");
2704
+ return { bvid, cid, duration: info.duration, mime: "audio/mp4", data: Buffer.concat(chunks, length) };
2705
+ }
2671
2706
  async getVideoComments(bvid, options = {}) {
2672
2707
  const info = await this.info(bvid);
2673
2708
  const limit = Math.max(1, Math.min(options.limit ?? 30, 60));
@@ -3138,6 +3173,9 @@ var init_internal = __esm({
3138
3173
  getVideoSubtitle(bvid) {
3139
3174
  return this.videoToolkitAPI.getVideoSubtitle(bvid);
3140
3175
  }
3176
+ getVideoAudio(bvid, options) {
3177
+ return this.videoToolkitAPI.getVideoAudio(bvid, options);
3178
+ }
3141
3179
  getVideoComments(bvid, options) {
3142
3180
  return this.videoToolkitAPI.getVideoComments(bvid, options);
3143
3181
  }