@ziplayer/ytexecplug 0.0.1
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 +216 -0
- package/dist/YouTubePlugin.d.ts +1 -0
- package/dist/YouTubePlugin.d.ts.map +1 -0
- package/dist/YouTubePlugin.js +343 -0
- package/dist/YouTubePlugin.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +104 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
- package/src/index.ts +103 -0
- package/tsconfig.json +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
<img width="1175" height="305" alt="logo" src="https://raw.githubusercontent.com/ZiProject/ZiPlayer/refs/heads/main/publish/logo.png" />
|
|
2
|
+
|
|
3
|
+
# @ziplayer/plugin
|
|
4
|
+
|
|
5
|
+
Official plugin bundle for ZiPlayer. It ships a set of ready‑to‑use source plugins you can register on your `PlayerManager`:
|
|
6
|
+
|
|
7
|
+
- YouTubePlugin: search + stream YouTube videos and playlists
|
|
8
|
+
- SoundCloudPlugin: search + stream SoundCloud tracks and sets
|
|
9
|
+
- SpotifyPlugin: resolve tracks/albums/playlists, stream via fallbacks
|
|
10
|
+
- TTSPlugin: Text‑to‑Speech playback from simple `tts:` queries
|
|
11
|
+
- AttachmentsPlugin: handle Discord attachment URLs and direct audio file URLs
|
|
12
|
+
|
|
13
|
+
ZiPlayer is an audio player built on top of `@discordjs/voice` and `discord.js`. This package provides sources; the core player
|
|
14
|
+
lives in `ziplayer`.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @ziplayer/plugin ziplayer @discordjs/voice discord.js
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The TTS plugin uses a lightweight Google TTS wrapper and HTTP fetches:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @zibot/zitts axios
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { PlayerManager } from "ziplayer";
|
|
32
|
+
import { YouTubePlugin, SoundCloudPlugin, SpotifyPlugin, TTSPlugin, AttachmentsPlugin } from "@ziplayer/plugin";
|
|
33
|
+
import { YTexec } from "@ziplayer/ytexecplug";
|
|
34
|
+
|
|
35
|
+
const ytbplg = new YouTubePlugin({ player: null });
|
|
36
|
+
|
|
37
|
+
ytbplg.getStream = new YTexec().getStream;
|
|
38
|
+
|
|
39
|
+
//create Player Manager
|
|
40
|
+
const manager = new PlayerManager({
|
|
41
|
+
plugins: [new TTSPlugin(), ytbplg, new SoundCloudPlugin(), new SpotifyPlugin(), new AttachmentsPlugin()],
|
|
42
|
+
extensions: [new lyricsExt(), new voiceExt(null, { client, minimalVoiceMessageDuration: 1 })],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Create and connect a player (discord.js VoiceChannel instance)
|
|
46
|
+
const player = await manager.create(guildId, { userdata: { channel: textChannel } });
|
|
47
|
+
await player.connect(voiceChannel);
|
|
48
|
+
|
|
49
|
+
// Search & play
|
|
50
|
+
await player.play("never gonna give you up", requestedBy);
|
|
51
|
+
|
|
52
|
+
// Play a playlist URL directly
|
|
53
|
+
await player.play("https://www.youtube.com/playlist?list=...", requestedBy);
|
|
54
|
+
|
|
55
|
+
// Speak with TTS
|
|
56
|
+
await player.play("tts:en:Hello there!", requestedBy);
|
|
57
|
+
|
|
58
|
+
// Play Discord attachment
|
|
59
|
+
await player.play("https://cdn.discordapp.com/attachments/123/456/audio.mp3", requestedBy);
|
|
60
|
+
|
|
61
|
+
// Handle events via the manager
|
|
62
|
+
manager.on("trackStart", (plr, track) => {
|
|
63
|
+
plr.userdata?.channel?.send?.(`Now playing: ${track.title}`);
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Included Plugins
|
|
68
|
+
|
|
69
|
+
### YouTubePlugin
|
|
70
|
+
|
|
71
|
+
- Resolves YouTube videos and playlists.
|
|
72
|
+
- Uses `youtubei.js` under the hood.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { YouTubePlugin } from "@ziplayer/plugin";
|
|
76
|
+
const youtube = new YouTubePlugin();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### SoundCloudPlugin
|
|
80
|
+
|
|
81
|
+
- Resolves tracks and sets. You may further tune streaming by combining with other plugins that provide fallbacks.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { SoundCloudPlugin } from "@ziplayer/plugin";
|
|
85
|
+
const sc = new SoundCloudPlugin();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### SpotifyPlugin
|
|
89
|
+
|
|
90
|
+
- Resolves track/album/playlist metadata from Spotify.
|
|
91
|
+
- Streaming typically uses fallback sources (e.g., YouTube) discovered by your plugin set.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { SpotifyPlugin } from "@ziplayer/plugin";
|
|
95
|
+
const sp = new SpotifyPlugin();
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### TTSPlugin (Text‑to‑Speech)
|
|
99
|
+
|
|
100
|
+
- Plays spoken audio from text using a lightweight Google TTS wrapper.
|
|
101
|
+
- **Accurate duration analysis**: Generates sample audio to measure actual duration instead of estimating.
|
|
102
|
+
- Supported query formats:
|
|
103
|
+
- `tts: <text>`
|
|
104
|
+
- `tts:<lang>:<text>` (e.g., `tts:vi:xin chao`)
|
|
105
|
+
- `tts:<lang>:1:<text>` (set `slow = true`, `0` = normal)
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { TTSPlugin } from "@ziplayer/plugin";
|
|
109
|
+
const tts = new TTSPlugin({ defaultLang: "en", slow: false });
|
|
110
|
+
|
|
111
|
+
// The plugin automatically analyzes TTS duration
|
|
112
|
+
const result = await tts.search("tts:en:Hello world", "user123");
|
|
113
|
+
console.log(`Duration: ${result.tracks[0].duration}s`); // Real duration from audio analysis
|
|
114
|
+
console.log(`Language: ${result.tracks[0].metadata.language}`); // "en"
|
|
115
|
+
console.log(`Slow mode: ${result.tracks[0].metadata.slowMode}`); // false
|
|
116
|
+
|
|
117
|
+
await player.play("tts:en:1:good morning", requestedBy);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Note: Please comply with the service’s terms and provide your own quotas. The wrapper is intended for lightweight usage and may
|
|
121
|
+
change without notice.
|
|
122
|
+
|
|
123
|
+
Advanced: custom TTS provider
|
|
124
|
+
|
|
125
|
+
You can override audio generation by passing a `createStream` function. It receives the text and context and can return a Node
|
|
126
|
+
`Readable`, an HTTP(S) URL string, or a `Buffer`.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const tts = new TTSPlugin({
|
|
130
|
+
defaultLang: "vi",
|
|
131
|
+
async createStream(text, ctx) {
|
|
132
|
+
// Example: integrate with Azure, CAMB.AI, etc.
|
|
133
|
+
// Return a URL and the plugin will stream it
|
|
134
|
+
const url = await myTTSService(text, { lang: ctx?.lang, slow: ctx?.slow });
|
|
135
|
+
return url; // or Readable / Buffer
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### AttachmentsPlugin
|
|
141
|
+
|
|
142
|
+
- Handles Discord attachment URLs and direct audio file URLs.
|
|
143
|
+
- Supports various audio formats (mp3, wav, ogg, m4a, flac, etc.).
|
|
144
|
+
- **Audio metadata analysis**: Extracts duration, title, artist, album, bitrate, etc.
|
|
145
|
+
- Includes file size validation and proper error handling.
|
|
146
|
+
- Uses Range requests to efficiently analyze metadata without downloading entire files.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { AttachmentsPlugin } from "@ziplayer/plugin";
|
|
150
|
+
const attachments = new AttachmentsPlugin({
|
|
151
|
+
maxFileSize: 25 * 1024 * 1024, // 25MB
|
|
152
|
+
allowedExtensions: ["mp3", "wav", "ogg", "m4a", "flac"],
|
|
153
|
+
debug: true, // Enable to see metadata analysis process
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// The plugin automatically analyzes audio metadata
|
|
157
|
+
const result = await attachments.search("https://cdn.discordapp.com/attachments/123/456/song.mp3", "user123");
|
|
158
|
+
console.log(`Duration: ${result.tracks[0].duration}s`); // Real duration from metadata
|
|
159
|
+
console.log(`Title: ${result.tracks[0].title}`); // May be extracted from metadata
|
|
160
|
+
console.log(`Artist: ${result.tracks[0].metadata.artist}`); // From metadata
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Writing Your Own Plugin
|
|
164
|
+
|
|
165
|
+
Plugins implement the `BasePlugin` contract from `ziplayer`:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import { BasePlugin, Track, SearchResult, StreamInfo } from "ziplayer";
|
|
169
|
+
|
|
170
|
+
export class MyPlugin extends BasePlugin {
|
|
171
|
+
name = "myplugin";
|
|
172
|
+
version = "1.0.0";
|
|
173
|
+
|
|
174
|
+
canHandle(query: string): boolean {
|
|
175
|
+
// Return true if this plugin can handle a given query/URL
|
|
176
|
+
return query.includes("mysite.com");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async search(query: string, requestedBy: string): Promise<SearchResult> {
|
|
180
|
+
// Return one or more tracks for the query
|
|
181
|
+
return {
|
|
182
|
+
tracks: [
|
|
183
|
+
{
|
|
184
|
+
id: "abc",
|
|
185
|
+
title: "My Track",
|
|
186
|
+
url: "https://mysite.com/track/abc",
|
|
187
|
+
duration: 180,
|
|
188
|
+
requestedBy,
|
|
189
|
+
source: this.name,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async getStream(track: Track): Promise<StreamInfo> {
|
|
196
|
+
// Return a Node Readable stream and an input type
|
|
197
|
+
return { stream, type: "arbitrary" };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Tips
|
|
203
|
+
|
|
204
|
+
- Keep network calls bounded; ZiPlayer applies timeouts to extractor operations.
|
|
205
|
+
- For sources that require indirection (like Spotify), consider a `getFallback` strategy via other plugins.
|
|
206
|
+
- Use `track.metadata` for any source‑specific fields you want to carry along.
|
|
207
|
+
|
|
208
|
+
## Requirements
|
|
209
|
+
|
|
210
|
+
- Node.js 18+
|
|
211
|
+
- `discord.js` 14 and `@discordjs/voice` 0.19+
|
|
212
|
+
- For TTS: `@zibot/zitts` and `axios`
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=YouTubePlugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"YouTubePlugin.d.ts","sourceRoot":"","sources":["../src/YouTubePlugin.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// import { BasePlugin, Track, SearchResult, StreamInfo, Player } from "ziplayer";
|
|
3
|
+
// export interface PluginOptions {
|
|
4
|
+
// player: Player;
|
|
5
|
+
// debug?: boolean;
|
|
6
|
+
// }
|
|
7
|
+
// /**
|
|
8
|
+
// * A plugin for handling YouTube audio content including videos, playlists, and search functionality.
|
|
9
|
+
// *
|
|
10
|
+
// * This plugin provides comprehensive support for:
|
|
11
|
+
// * - YouTube video URLs (youtube.com, youtu.be, music.youtube.com)
|
|
12
|
+
// * - YouTube playlist URLs and dynamic mixes
|
|
13
|
+
// * - YouTube search queries
|
|
14
|
+
// * - Audio stream extraction from YouTube videos
|
|
15
|
+
// * - Related track recommendations
|
|
16
|
+
// *
|
|
17
|
+
// * @example
|
|
18
|
+
// * const youtubePlugin = new YouTubePlugin();
|
|
19
|
+
// *
|
|
20
|
+
// * // Add to PlayerManager
|
|
21
|
+
// * const manager = new PlayerManager({
|
|
22
|
+
// * plugins: [youtubePlugin]
|
|
23
|
+
// * });
|
|
24
|
+
// *
|
|
25
|
+
// * // Search for videos
|
|
26
|
+
// * const result = await youtubePlugin.search("Never Gonna Give You Up", "user123");
|
|
27
|
+
// *
|
|
28
|
+
// * // Get audio stream
|
|
29
|
+
// * const stream = await youtubePlugin.getStream(result.tracks[0]);
|
|
30
|
+
// *
|
|
31
|
+
// * @since 1.0.0
|
|
32
|
+
// */
|
|
33
|
+
// export class YouTubePlugin extends BasePlugin {
|
|
34
|
+
// name = "youtube";
|
|
35
|
+
// version = "1.0.0";
|
|
36
|
+
// private ready: Promise<void>;
|
|
37
|
+
// private player: Player | undefined;
|
|
38
|
+
// private options: PluginOptions;
|
|
39
|
+
// /**
|
|
40
|
+
// * Creates a new YouTubePlugin instance.
|
|
41
|
+
// *
|
|
42
|
+
// * The plugin will automatically initialize YouTube clients for both video playback
|
|
43
|
+
// * and search functionality. Initialization is asynchronous and handled internally.
|
|
44
|
+
// *
|
|
45
|
+
// * @example
|
|
46
|
+
// * const plugin = new YouTubePlugin();
|
|
47
|
+
// * // Plugin is ready to use after initialization completes
|
|
48
|
+
// */
|
|
49
|
+
// constructor(options: PluginOptions) {
|
|
50
|
+
// super();
|
|
51
|
+
// this.player = options?.player ?? undefined;
|
|
52
|
+
// this.options = options ?? {};
|
|
53
|
+
// this.ready = this.init();
|
|
54
|
+
// }
|
|
55
|
+
// private async init(): Promise<void> {}
|
|
56
|
+
// private debug(message?: any, ...optionalParams: any[]): void {
|
|
57
|
+
// if (this.options?.debug && this?.player && this.player?.listenerCount("debug") > 0) {
|
|
58
|
+
// this.player.emit("debug", `[YouTubePlugin] ${message}`, ...optionalParams);
|
|
59
|
+
// }
|
|
60
|
+
// }
|
|
61
|
+
// // Build a Track from various YouTube object shapes (search item, playlist item, watch_next feed, basic_info, info)
|
|
62
|
+
// private buildTrack(raw: any, requestedBy: string, extra?: { playlist?: string }): Track {
|
|
63
|
+
// const pickFirst = (...vals: any[]) => vals.find((v) => v !== undefined && v !== null && v !== "");
|
|
64
|
+
// // Try to resolve from multiple common shapes
|
|
65
|
+
// const id = pickFirst(
|
|
66
|
+
// raw?.id,
|
|
67
|
+
// raw?.video_id,
|
|
68
|
+
// raw?.videoId,
|
|
69
|
+
// raw?.content_id,
|
|
70
|
+
// raw?.identifier,
|
|
71
|
+
// raw?.basic_info?.id,
|
|
72
|
+
// raw?.basic_info?.video_id,
|
|
73
|
+
// raw?.basic_info?.videoId,
|
|
74
|
+
// raw?.basic_info?.content_id,
|
|
75
|
+
// );
|
|
76
|
+
// const title = pickFirst(
|
|
77
|
+
// raw?.metadata?.title?.text,
|
|
78
|
+
// raw?.title?.text,
|
|
79
|
+
// raw?.title,
|
|
80
|
+
// raw?.headline,
|
|
81
|
+
// raw?.basic_info?.title,
|
|
82
|
+
// "Unknown title",
|
|
83
|
+
// );
|
|
84
|
+
// const durationValue = pickFirst(
|
|
85
|
+
// raw?.length_seconds,
|
|
86
|
+
// raw?.duration?.seconds,
|
|
87
|
+
// raw?.duration?.text,
|
|
88
|
+
// raw?.duration,
|
|
89
|
+
// raw?.length_text,
|
|
90
|
+
// raw?.basic_info?.duration,
|
|
91
|
+
// );
|
|
92
|
+
// const duration = Number(toSeconds(durationValue)) || 0;
|
|
93
|
+
// const thumb = pickFirst(
|
|
94
|
+
// raw?.thumbnails?.[0]?.url,
|
|
95
|
+
// raw?.thumbnail?.[0]?.url,
|
|
96
|
+
// raw?.thumbnail?.url,
|
|
97
|
+
// raw?.thumbnail?.thumbnails?.[0]?.url,
|
|
98
|
+
// raw?.content_image?.image?.[0]?.url,
|
|
99
|
+
// raw?.basic_info?.thumbnail?.[0]?.url,
|
|
100
|
+
// raw?.basic_info?.thumbnail?.[raw?.basic_info?.thumbnail?.length - 1]?.url,
|
|
101
|
+
// raw?.thumbnails?.[raw?.thumbnails?.length - 1]?.url,
|
|
102
|
+
// );
|
|
103
|
+
// const author = pickFirst(raw?.author?.name, raw?.author, raw?.channel?.name, raw?.owner?.name, raw?.basic_info?.author);
|
|
104
|
+
// const views = pickFirst(
|
|
105
|
+
// raw?.view_count,
|
|
106
|
+
// raw?.views,
|
|
107
|
+
// raw?.short_view_count,
|
|
108
|
+
// raw?.stats?.view_count,
|
|
109
|
+
// raw?.basic_info?.view_count,
|
|
110
|
+
// );
|
|
111
|
+
// const url = pickFirst(raw?.url, id ? `https://www.youtube.com/watch?v=${id}` : undefined);
|
|
112
|
+
// this.debug("Track build:", {
|
|
113
|
+
// id: String(id),
|
|
114
|
+
// title: String(title),
|
|
115
|
+
// url: String(url),
|
|
116
|
+
// duration,
|
|
117
|
+
// thumbnail: thumb,
|
|
118
|
+
// requestedBy,
|
|
119
|
+
// source: this.name,
|
|
120
|
+
// });
|
|
121
|
+
// return {
|
|
122
|
+
// id: String(id),
|
|
123
|
+
// title: String(title),
|
|
124
|
+
// url: String(url),
|
|
125
|
+
// duration,
|
|
126
|
+
// thumbnail: thumb,
|
|
127
|
+
// requestedBy,
|
|
128
|
+
// source: this.name,
|
|
129
|
+
// metadata: {
|
|
130
|
+
// author,
|
|
131
|
+
// views,
|
|
132
|
+
// ...(extra?.playlist ? { playlist: extra.playlist } : {}),
|
|
133
|
+
// },
|
|
134
|
+
// } as Track;
|
|
135
|
+
// }
|
|
136
|
+
// /**
|
|
137
|
+
// * Determines if this plugin can handle the given query.
|
|
138
|
+
// *
|
|
139
|
+
// * @param query - The search query or URL to check
|
|
140
|
+
// * @returns `true` if the plugin can handle the query, `false` otherwise
|
|
141
|
+
// *
|
|
142
|
+
// * @example
|
|
143
|
+
// * plugin.canHandle("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); // true
|
|
144
|
+
// * plugin.canHandle("Never Gonna Give You Up"); // true
|
|
145
|
+
// * plugin.canHandle("spotify:track:123"); // false
|
|
146
|
+
// */
|
|
147
|
+
// canHandle(query: string): boolean {
|
|
148
|
+
// const q = (query || "").trim().toLowerCase();
|
|
149
|
+
// const isUrl = q.startsWith("http://") || q.startsWith("https://");
|
|
150
|
+
// if (isUrl) {
|
|
151
|
+
// try {
|
|
152
|
+
// const parsed = new URL(query);
|
|
153
|
+
// const allowedHosts = ["youtube.com", "www.youtube.com", "music.youtube.com", "youtu.be", "www.youtu.be"];
|
|
154
|
+
// return allowedHosts.includes(parsed.hostname.toLowerCase());
|
|
155
|
+
// } catch (e) {
|
|
156
|
+
// return false;
|
|
157
|
+
// }
|
|
158
|
+
// }
|
|
159
|
+
// // Avoid intercepting explicit patterns for other extractors
|
|
160
|
+
// if (q.startsWith("tts:") || q.startsWith("say ")) return false;
|
|
161
|
+
// if (q.startsWith("spotify:") || q.includes("open.spotify.com")) return false;
|
|
162
|
+
// if (q.includes("soundcloud")) return false;
|
|
163
|
+
// // Treat remaining non-URL free text as YouTube-searchable
|
|
164
|
+
// return true;
|
|
165
|
+
// }
|
|
166
|
+
// /**
|
|
167
|
+
// * Validates if a URL is a valid YouTube URL.
|
|
168
|
+
// *
|
|
169
|
+
// * @param url - The URL to validate
|
|
170
|
+
// * @returns `true` if the URL is a valid YouTube URL, `false` otherwise
|
|
171
|
+
// *
|
|
172
|
+
// * @example
|
|
173
|
+
// * plugin.validate("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); // true
|
|
174
|
+
// * plugin.validate("https://youtu.be/dQw4w9WgXcQ"); // true
|
|
175
|
+
// * plugin.validate("https://spotify.com/track/123"); // false
|
|
176
|
+
// */
|
|
177
|
+
// validate(url: string): boolean {
|
|
178
|
+
// try {
|
|
179
|
+
// const parsed = new URL(url);
|
|
180
|
+
// const allowedHosts = ["youtube.com", "www.youtube.com", "music.youtube.com", "youtu.be", "www.youtu.be", "m.youtube.com"];
|
|
181
|
+
// return allowedHosts.includes(parsed.hostname.toLowerCase());
|
|
182
|
+
// } catch (e) {
|
|
183
|
+
// return false;
|
|
184
|
+
// }
|
|
185
|
+
// }
|
|
186
|
+
// /**
|
|
187
|
+
// * Retrieves the audio stream for a YouTube track using sabr download.
|
|
188
|
+
// *
|
|
189
|
+
// * This method extracts the audio stream from a YouTube video using the sabr download
|
|
190
|
+
// * method which provides better quality and more reliable streaming.
|
|
191
|
+
// *
|
|
192
|
+
// * @param track - The Track object to get the stream for
|
|
193
|
+
// * @returns A StreamInfo object containing the audio stream and metadata
|
|
194
|
+
// * @throws {Error} If the track ID is invalid or stream extraction fails
|
|
195
|
+
// *
|
|
196
|
+
// * @example
|
|
197
|
+
// * const track = { id: "dQw4w9WgXcQ", title: "Never Gonna Give You Up", ... };
|
|
198
|
+
// * const streamInfo = await plugin.getStream(track);
|
|
199
|
+
// * console.log(streamInfo.type); // "arbitrary"
|
|
200
|
+
// * console.log(streamInfo.stream); // Readable stream
|
|
201
|
+
// */
|
|
202
|
+
// async getStream(track: Track): Promise<StreamInfo> {
|
|
203
|
+
// await this.ready;
|
|
204
|
+
// const id = this.extractVideoId(track.url) || track.id;
|
|
205
|
+
// if (!id) throw new Error("Invalid track id");
|
|
206
|
+
// try {
|
|
207
|
+
// this.debug("🚀 Attempting sabr download for video ID:", id);
|
|
208
|
+
// // Use sabr download for better quality and reliability
|
|
209
|
+
// const { streamResults } = await createSabrStream(id, DEFAULT_SABR_OPTIONS);
|
|
210
|
+
// const { audioStream, selectedFormats, videoTitle } = streamResults;
|
|
211
|
+
// this.debug("✅ Sabr download successful, converting Web Stream to Node.js Stream");
|
|
212
|
+
// // Convert Web Stream to Node.js Readable Stream
|
|
213
|
+
// const nodeStream = webStreamToNodeStream(audioStream);
|
|
214
|
+
// this.debug("✅ Stream conversion complete, returning Node.js stream");
|
|
215
|
+
// // Return the converted Node.js stream
|
|
216
|
+
// return {
|
|
217
|
+
// stream: nodeStream,
|
|
218
|
+
// type: "arbitrary",
|
|
219
|
+
// metadata: {
|
|
220
|
+
// ...track.metadata,
|
|
221
|
+
// itag: selectedFormats.audioFormat.itag,
|
|
222
|
+
// mime: selectedFormats.audioFormat.mimeType,
|
|
223
|
+
// },
|
|
224
|
+
// };
|
|
225
|
+
// } catch (e: any) {
|
|
226
|
+
// this.debug("⚠️ Sabr download failed, falling back to youtubei.js:", e.message);
|
|
227
|
+
// // Fallback to original youtubei.js method if sabr download fails
|
|
228
|
+
// try {
|
|
229
|
+
// const stream: any = await (this.client as any).download(id, {
|
|
230
|
+
// type: "audio",
|
|
231
|
+
// quality: "best",
|
|
232
|
+
// });
|
|
233
|
+
// // Check if it's a Web Stream and convert it
|
|
234
|
+
// this.debug("🔍 Checking stream type:", typeof stream, stream?.constructor?.name);
|
|
235
|
+
// if (stream && typeof stream.getReader === "function") {
|
|
236
|
+
// this.debug("🔄 Converting Web Stream to Node.js Stream");
|
|
237
|
+
// const nodeStream = webStreamToNodeStream(stream);
|
|
238
|
+
// this.debug("✅ Stream converted successfully");
|
|
239
|
+
// return {
|
|
240
|
+
// stream: nodeStream,
|
|
241
|
+
// type: "arbitrary",
|
|
242
|
+
// metadata: track.metadata,
|
|
243
|
+
// };
|
|
244
|
+
// } else {
|
|
245
|
+
// this.debug("⚠️ Stream is not a Web Stream or is null");
|
|
246
|
+
// }
|
|
247
|
+
// return {
|
|
248
|
+
// stream,
|
|
249
|
+
// type: "arbitrary",
|
|
250
|
+
// metadata: track.metadata,
|
|
251
|
+
// };
|
|
252
|
+
// } catch (fallbackError: any) {
|
|
253
|
+
// try {
|
|
254
|
+
// const info: any = await (this.client as any).getBasicInfo(id);
|
|
255
|
+
// // Prefer m4a audio-only formats first
|
|
256
|
+
// let format: any = info?.chooseFormat?.({
|
|
257
|
+
// type: "audio",
|
|
258
|
+
// quality: "best",
|
|
259
|
+
// });
|
|
260
|
+
// if (!format && info?.formats?.length) {
|
|
261
|
+
// const audioOnly = info.formats.filter((f: any) => f.mime_type?.includes("audio"));
|
|
262
|
+
// audioOnly.sort((a: any, b: any) => (b.bitrate || 0) - (a.bitrate || 0));
|
|
263
|
+
// format = audioOnly[0];
|
|
264
|
+
// }
|
|
265
|
+
// if (!format) throw new Error("No audio format available");
|
|
266
|
+
// let url: string | undefined = undefined;
|
|
267
|
+
// if (typeof format.decipher === "function") {
|
|
268
|
+
// url = format.decipher((this.client as any).session.player);
|
|
269
|
+
// }
|
|
270
|
+
// if (!url) url = format.url;
|
|
271
|
+
// if (!url) throw new Error("No valid URL to decipher");
|
|
272
|
+
// const res = await fetch(url);
|
|
273
|
+
// if (!res.ok || !res.body) {
|
|
274
|
+
// throw new Error(`HTTP ${res.status}`);
|
|
275
|
+
// }
|
|
276
|
+
// // Convert Web Stream to Node.js Stream
|
|
277
|
+
// this.debug("🔄 Converting fetch response Web Stream to Node.js Stream");
|
|
278
|
+
// const nodeStream = webStreamToNodeStream(res.body);
|
|
279
|
+
// return {
|
|
280
|
+
// stream: nodeStream,
|
|
281
|
+
// type: "arbitrary",
|
|
282
|
+
// metadata: {
|
|
283
|
+
// ...track.metadata,
|
|
284
|
+
// itag: format.itag,
|
|
285
|
+
// mime: format.mime_type,
|
|
286
|
+
// },
|
|
287
|
+
// };
|
|
288
|
+
// } catch (inner: any) {
|
|
289
|
+
// throw new Error(`Failed to get YouTube stream: ${inner?.message || inner}`);
|
|
290
|
+
// }
|
|
291
|
+
// }
|
|
292
|
+
// }
|
|
293
|
+
// }
|
|
294
|
+
// async getFallback(track: Track): Promise<StreamInfo> {
|
|
295
|
+
// try {
|
|
296
|
+
// const result = await this.search(track.title, track.requestedBy);
|
|
297
|
+
// const first = result.tracks[0];
|
|
298
|
+
// this.debug("Fallback track:", first);
|
|
299
|
+
// if (!first) throw new Error("No fallback track found");
|
|
300
|
+
// return await this.getStream(first);
|
|
301
|
+
// } catch (e: any) {
|
|
302
|
+
// throw new Error(`YouTube fallback search failed: ${e?.message || e}`);
|
|
303
|
+
// }
|
|
304
|
+
// }
|
|
305
|
+
// private extractVideoId(input: string): string | null {
|
|
306
|
+
// try {
|
|
307
|
+
// const u = new URL(input);
|
|
308
|
+
// const allowedShortHosts = ["youtu.be"];
|
|
309
|
+
// const allowedLongHosts = ["youtube.com", "www.youtube.com", "music.youtube.com", "m.youtube.com"];
|
|
310
|
+
// if (allowedShortHosts.includes(u.hostname)) {
|
|
311
|
+
// return u.pathname.split("/").filter(Boolean)[0] || null;
|
|
312
|
+
// }
|
|
313
|
+
// if (allowedLongHosts.includes(u.hostname)) {
|
|
314
|
+
// // watch?v=, shorts/, embed/
|
|
315
|
+
// if (u.searchParams.get("v")) return u.searchParams.get("v");
|
|
316
|
+
// const path = u.pathname;
|
|
317
|
+
// if (path.startsWith("/shorts/")) return path.replace("/shorts/", "");
|
|
318
|
+
// if (path.startsWith("/embed/")) return path.replace("/embed/", "");
|
|
319
|
+
// }
|
|
320
|
+
// return null;
|
|
321
|
+
// } catch {
|
|
322
|
+
// return null;
|
|
323
|
+
// }
|
|
324
|
+
// }
|
|
325
|
+
// }
|
|
326
|
+
// function toSeconds(d: any): number | undefined {
|
|
327
|
+
// if (typeof d === "number") return d;
|
|
328
|
+
// if (typeof d === "string") {
|
|
329
|
+
// // mm:ss or hh:mm:ss
|
|
330
|
+
// const parts = d.split(":").map(Number);
|
|
331
|
+
// if (parts.some((n) => Number.isNaN(n))) return undefined;
|
|
332
|
+
// if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
333
|
+
// if (parts.length === 2) return parts[0] * 60 + parts[1];
|
|
334
|
+
// const asNum = Number(d);
|
|
335
|
+
// return Number.isFinite(asNum) ? asNum : undefined;
|
|
336
|
+
// }
|
|
337
|
+
// if (d && typeof d === "object") {
|
|
338
|
+
// if (typeof (d as any).seconds === "number") return (d as any).seconds;
|
|
339
|
+
// if (typeof (d as any).milliseconds === "number") return Math.floor((d as any).milliseconds / 1000);
|
|
340
|
+
// }
|
|
341
|
+
// return undefined;
|
|
342
|
+
// }
|
|
343
|
+
//# sourceMappingURL=YouTubePlugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"YouTubePlugin.js","sourceRoot":"","sources":["../src/YouTubePlugin.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BasePlugin } from "ziplayer";
|
|
2
|
+
import { Track, SearchResult, StreamInfo } from "ziplayer";
|
|
3
|
+
import { Readable } from "stream";
|
|
4
|
+
/**
|
|
5
|
+
* Converts a Web ReadableStream to a Node.js Readable stream
|
|
6
|
+
*/
|
|
7
|
+
export declare function webStreamToNodeStream(webStream: ReadableStream): Readable;
|
|
8
|
+
export declare class YTexec extends BasePlugin {
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
canHandle(query: string): boolean;
|
|
12
|
+
search(query: string, requestedBy: string): Promise<SearchResult>;
|
|
13
|
+
getStream(track: Track): Promise<StreamInfo>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAGlC;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,cAAc,GAAG,QAAQ,CA8BzE;AAoBD,qBAAa,MAAO,SAAQ,UAAU;IACrC,IAAI,SAAY;IAChB,OAAO,SAAW;IAElB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAe3B,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAIjE,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,UAAU,CAAC;CAqBlD"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.YTexec = void 0;
|
|
7
|
+
exports.webStreamToNodeStream = webStreamToNodeStream;
|
|
8
|
+
const ziplayer_1 = require("ziplayer");
|
|
9
|
+
const stream_1 = require("stream");
|
|
10
|
+
const youtube_dl_exec_1 = __importDefault(require("youtube-dl-exec"));
|
|
11
|
+
/**
|
|
12
|
+
* Converts a Web ReadableStream to a Node.js Readable stream
|
|
13
|
+
*/
|
|
14
|
+
function webStreamToNodeStream(webStream) {
|
|
15
|
+
const nodeStream = new stream_1.Readable({
|
|
16
|
+
read() {
|
|
17
|
+
// This will be handled by the Web Stream reader
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
// Create a reader from the Web Stream
|
|
21
|
+
const reader = webStream.getReader();
|
|
22
|
+
// Read chunks and push to Node.js stream
|
|
23
|
+
const pump = async () => {
|
|
24
|
+
try {
|
|
25
|
+
while (true) {
|
|
26
|
+
const { done, value } = await reader.read();
|
|
27
|
+
if (done) {
|
|
28
|
+
nodeStream.push(null); // End the stream
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
nodeStream.push(Buffer.from(value));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
nodeStream.destroy(error);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
// Start pumping data
|
|
39
|
+
pump();
|
|
40
|
+
return nodeStream;
|
|
41
|
+
}
|
|
42
|
+
async function getYoutubeStream(url) {
|
|
43
|
+
const info = await (0, youtube_dl_exec_1.default)(url, {
|
|
44
|
+
dumpSingleJson: true,
|
|
45
|
+
noCheckCertificates: true,
|
|
46
|
+
noWarnings: true,
|
|
47
|
+
preferFreeFormats: true,
|
|
48
|
+
format: "bestaudio/best",
|
|
49
|
+
addHeader: ["referer:youtube.com", "user-agent:googlebot"],
|
|
50
|
+
});
|
|
51
|
+
const videourl = typeof info === "object" ? info?.url : info;
|
|
52
|
+
if (!videourl) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return videourl;
|
|
56
|
+
}
|
|
57
|
+
class YTexec extends ziplayer_1.BasePlugin {
|
|
58
|
+
constructor() {
|
|
59
|
+
super(...arguments);
|
|
60
|
+
this.name = "YTexec";
|
|
61
|
+
this.version = "1.0.0";
|
|
62
|
+
}
|
|
63
|
+
canHandle(query) {
|
|
64
|
+
const q = (query || "").trim().toLowerCase();
|
|
65
|
+
const isUrl = q.startsWith("http://") || q.startsWith("https://");
|
|
66
|
+
if (isUrl) {
|
|
67
|
+
try {
|
|
68
|
+
const parsed = new URL(query);
|
|
69
|
+
const allowedHosts = ["youtube.com", "www.youtube.com", "music.youtube.com", "youtu.be", "www.youtu.be"];
|
|
70
|
+
return allowedHosts.includes(parsed.hostname.toLowerCase());
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
async search(query, requestedBy) {
|
|
79
|
+
return { tracks: [] };
|
|
80
|
+
}
|
|
81
|
+
async getStream(track) {
|
|
82
|
+
try {
|
|
83
|
+
const youtubeUrl = await getYoutubeStream(track.url);
|
|
84
|
+
if (!youtubeUrl) {
|
|
85
|
+
throw new Error("Failed to get YouTube stream URL");
|
|
86
|
+
}
|
|
87
|
+
const response = await fetch(youtubeUrl);
|
|
88
|
+
if (!response.ok || !response.body) {
|
|
89
|
+
throw new Error("Failed to fetch YouTube stream");
|
|
90
|
+
}
|
|
91
|
+
const stream = webStreamToNodeStream(response.body);
|
|
92
|
+
return {
|
|
93
|
+
stream,
|
|
94
|
+
type: "arbitrary",
|
|
95
|
+
metadata: track.metadata,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
throw new Error(`Failed to get YouTube stream: ${error}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.YTexec = YTexec;
|
|
104
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ziplayer/ytexecplug",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A modular Discord voice player with plugin system",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ZiPlayer",
|
|
7
|
+
"@ziplayer/plugin",
|
|
8
|
+
"discord",
|
|
9
|
+
"music",
|
|
10
|
+
"player",
|
|
11
|
+
"voice"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://player.ziji.world",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/ZiProject/ZiPlayer/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/ZiProject/ZiPlayer.git"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Ziji",
|
|
23
|
+
"main": "dist/index.js",
|
|
24
|
+
"types": "dist/index.d.ts",
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"dev": "tsc --watch",
|
|
28
|
+
"prepare": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"youtube-dl-exec": "^3.0.30",
|
|
32
|
+
"ziplayer": "^0.2.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^20.0.0",
|
|
36
|
+
"typescript": "^5.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { BasePlugin } from "ziplayer";
|
|
2
|
+
import { Track, SearchResult, StreamInfo } from "ziplayer";
|
|
3
|
+
import { Readable } from "stream";
|
|
4
|
+
import youtubedl from "youtube-dl-exec";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Converts a Web ReadableStream to a Node.js Readable stream
|
|
8
|
+
*/
|
|
9
|
+
export function webStreamToNodeStream(webStream: ReadableStream): Readable {
|
|
10
|
+
const nodeStream = new Readable({
|
|
11
|
+
read() {
|
|
12
|
+
// This will be handled by the Web Stream reader
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Create a reader from the Web Stream
|
|
17
|
+
const reader = webStream.getReader();
|
|
18
|
+
|
|
19
|
+
// Read chunks and push to Node.js stream
|
|
20
|
+
const pump = async () => {
|
|
21
|
+
try {
|
|
22
|
+
while (true) {
|
|
23
|
+
const { done, value } = await reader.read();
|
|
24
|
+
if (done) {
|
|
25
|
+
nodeStream.push(null); // End the stream
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
nodeStream.push(Buffer.from(value));
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
nodeStream.destroy(error as Error);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Start pumping data
|
|
36
|
+
pump();
|
|
37
|
+
|
|
38
|
+
return nodeStream;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function getYoutubeStream(url: string): Promise<string | null> {
|
|
42
|
+
const info = await youtubedl(url, {
|
|
43
|
+
dumpSingleJson: true,
|
|
44
|
+
noCheckCertificates: true,
|
|
45
|
+
noWarnings: true,
|
|
46
|
+
preferFreeFormats: true,
|
|
47
|
+
format: "bestaudio/best",
|
|
48
|
+
addHeader: ["referer:youtube.com", "user-agent:googlebot"],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const videourl = typeof info === "object" ? (info as any)?.url : info;
|
|
52
|
+
if (!videourl) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return videourl;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class YTexec extends BasePlugin {
|
|
60
|
+
name = "YTexec";
|
|
61
|
+
version = "1.0.0";
|
|
62
|
+
|
|
63
|
+
canHandle(query: string): boolean {
|
|
64
|
+
const q = (query || "").trim().toLowerCase();
|
|
65
|
+
const isUrl = q.startsWith("http://") || q.startsWith("https://");
|
|
66
|
+
if (isUrl) {
|
|
67
|
+
try {
|
|
68
|
+
const parsed = new URL(query);
|
|
69
|
+
const allowedHosts = ["youtube.com", "www.youtube.com", "music.youtube.com", "youtu.be", "www.youtu.be"];
|
|
70
|
+
return allowedHosts.includes(parsed.hostname.toLowerCase());
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async search(query: string, requestedBy: string): Promise<SearchResult> {
|
|
79
|
+
return { tracks: [] };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async getStream(track: Track): Promise<StreamInfo> {
|
|
83
|
+
try {
|
|
84
|
+
const youtubeUrl = await getYoutubeStream(track.url);
|
|
85
|
+
if (!youtubeUrl) {
|
|
86
|
+
throw new Error("Failed to get YouTube stream URL");
|
|
87
|
+
}
|
|
88
|
+
const response = await fetch(youtubeUrl);
|
|
89
|
+
if (!response.ok || !response.body) {
|
|
90
|
+
throw new Error("Failed to fetch YouTube stream");
|
|
91
|
+
}
|
|
92
|
+
const stream = webStreamToNodeStream(response.body as ReadableStream) as unknown as Readable;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
stream,
|
|
96
|
+
type: "arbitrary",
|
|
97
|
+
metadata: track.metadata,
|
|
98
|
+
};
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw new Error(`Failed to get YouTube stream: ${error}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"moduleResolution": "node",
|
|
16
|
+
"allowSyntheticDefaultImports": true,
|
|
17
|
+
"experimentalDecorators": true,
|
|
18
|
+
"emitDecoratorMetadata": true,
|
|
19
|
+
"resolveJsonModule": true
|
|
20
|
+
},
|
|
21
|
+
"include": ["src/*", "src/types/*"],
|
|
22
|
+
"exclude": ["node_modules", "dist", "examples"]
|
|
23
|
+
}
|