@ziplayer/plugin 0.0.2 → 0.0.4
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 +132 -97
- package/dist/SoundCloudPlugin.d.ts.map +1 -1
- package/dist/SoundCloudPlugin.js +38 -4
- package/dist/SoundCloudPlugin.js.map +1 -1
- package/dist/SpotifyPlugin.d.ts.map +1 -1
- package/dist/SpotifyPlugin.js +2 -2
- package/dist/SpotifyPlugin.js.map +1 -1
- package/dist/TTSPlugin.d.ts +36 -0
- package/dist/TTSPlugin.d.ts.map +1 -0
- package/dist/TTSPlugin.js +202 -0
- package/dist/TTSPlugin.js.map +1 -0
- package/dist/YouTubePlugin.d.ts +2 -0
- package/dist/YouTubePlugin.d.ts.map +1 -1
- package/dist/YouTubePlugin.js +113 -91
- package/dist/YouTubePlugin.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +41 -39
- package/src/SoundCloudPlugin.ts +219 -191
- package/src/SpotifyPlugin.ts +190 -204
- package/src/TTSPlugin.ts +234 -0
- package/src/YouTubePlugin.ts +382 -320
- package/src/index.ts +4 -3
- package/tsconfig.json +23 -23
package/package.json
CHANGED
|
@@ -1,39 +1,41 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@ziplayer/plugin",
|
|
3
|
-
"version": "0.0.
|
|
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://
|
|
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
|
-
"@zibot/scdl": "^0.0.5",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@ziplayer/plugin",
|
|
3
|
+
"version": "0.0.4",
|
|
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
|
+
"@zibot/scdl": "^0.0.5",
|
|
32
|
+
"@zibot/zitts": "^0.0.3",
|
|
33
|
+
"axios": "^1.11.0",
|
|
34
|
+
"youtubei.js": "^15.0.1",
|
|
35
|
+
"ziplayer": "^0.0.6"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.0.0",
|
|
39
|
+
"typescript": "^5.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/SoundCloudPlugin.ts
CHANGED
|
@@ -1,191 +1,219 @@
|
|
|
1
|
-
import { BasePlugin, Track, SearchResult, StreamInfo } from "ziplayer";
|
|
2
|
-
|
|
3
|
-
const SoundCloud = require("@zibot/scdl");
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
1
|
+
import { BasePlugin, Track, SearchResult, StreamInfo } from "ziplayer";
|
|
2
|
+
|
|
3
|
+
const SoundCloud = require("@zibot/scdl");
|
|
4
|
+
import { URL } from "url";
|
|
5
|
+
|
|
6
|
+
const ALLOWED_SOUNDCLOUD_HOSTS = ["soundcloud.com", "www.soundcloud.com", "m.soundcloud.com"];
|
|
7
|
+
|
|
8
|
+
function isValidSoundCloudHost(maybeUrl: string): boolean {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = new URL(maybeUrl);
|
|
11
|
+
return ALLOWED_SOUNDCLOUD_HOSTS.includes(parsed.hostname);
|
|
12
|
+
} catch {
|
|
13
|
+
// Not a valid URL, not handled as host-based
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class SoundCloudPlugin extends BasePlugin {
|
|
18
|
+
name = "soundcloud";
|
|
19
|
+
version = "1.0.0";
|
|
20
|
+
private client: any;
|
|
21
|
+
private ready: Promise<void>;
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
super();
|
|
25
|
+
this.ready = this.init();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private async init(): Promise<void> {
|
|
29
|
+
this.client = new SoundCloud({ init: false });
|
|
30
|
+
await this.client.init();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
canHandle(query: string): boolean {
|
|
34
|
+
const q = (query || "").trim().toLowerCase();
|
|
35
|
+
const isUrl = q.startsWith("http://") || q.startsWith("https://");
|
|
36
|
+
if (isUrl) {
|
|
37
|
+
return isValidSoundCloudHost(query);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Avoid intercepting explicit patterns for other extractors
|
|
41
|
+
if (q.startsWith("tts:") || q.startsWith("say ")) return false;
|
|
42
|
+
if (q.startsWith("spotify:") || q.includes("open.spotify.com")) return false;
|
|
43
|
+
if (q.includes("youtube")) return false;
|
|
44
|
+
|
|
45
|
+
// Treat remaining non-URL free text as searchable
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
validate(url: string): boolean {
|
|
50
|
+
return isValidSoundCloudHost(url);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async search(query: string, requestedBy: string): Promise<SearchResult> {
|
|
54
|
+
await this.ready;
|
|
55
|
+
|
|
56
|
+
// If the query is a URL but not a SoundCloud URL, do not handle it here
|
|
57
|
+
// This prevents hijacking e.g. YouTube/Spotify links as free-text searches.
|
|
58
|
+
try {
|
|
59
|
+
const q = (query || "").trim().toLowerCase();
|
|
60
|
+
const isUrl = q.startsWith("http://") || q.startsWith("https://");
|
|
61
|
+
if (isUrl && !this.validate(query)) {
|
|
62
|
+
return { tracks: [] };
|
|
63
|
+
}
|
|
64
|
+
} catch {}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
if (isValidSoundCloudHost(query)) {
|
|
68
|
+
try {
|
|
69
|
+
const info = await this.client.getTrackDetails(query);
|
|
70
|
+
const track: Track = {
|
|
71
|
+
id: info.id.toString(),
|
|
72
|
+
title: info.title,
|
|
73
|
+
url: info.permalink_url || query,
|
|
74
|
+
duration: info.duration,
|
|
75
|
+
thumbnail: info.artwork_url,
|
|
76
|
+
requestedBy,
|
|
77
|
+
source: this.name,
|
|
78
|
+
metadata: {
|
|
79
|
+
author: info.user?.username,
|
|
80
|
+
plays: info.playback_count,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
return { tracks: [track] };
|
|
84
|
+
} catch {
|
|
85
|
+
const playlist = await this.client.getPlaylistDetails(query);
|
|
86
|
+
const tracks: Track[] = playlist.tracks.map((t: any) => ({
|
|
87
|
+
id: t.id.toString(),
|
|
88
|
+
title: t.title,
|
|
89
|
+
url: t.permalink_url,
|
|
90
|
+
duration: t.duration,
|
|
91
|
+
thumbnail: t.artwork_url || playlist.artwork_url,
|
|
92
|
+
requestedBy,
|
|
93
|
+
source: this.name,
|
|
94
|
+
metadata: {
|
|
95
|
+
author: t.user?.username,
|
|
96
|
+
plays: t.playback_count,
|
|
97
|
+
playlist: playlist.id?.toString(),
|
|
98
|
+
},
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
tracks,
|
|
103
|
+
playlist: {
|
|
104
|
+
name: playlist.title,
|
|
105
|
+
url: playlist.permalink_url || query,
|
|
106
|
+
thumbnail: playlist.artwork_url,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const results = await this.client.searchTracks({ query, limit: 15 });
|
|
113
|
+
const tracks: Track[] = results.slice(0, 10).map((track: any) => ({
|
|
114
|
+
id: track.id.toString(),
|
|
115
|
+
title: track.title,
|
|
116
|
+
url: track.permalink_url,
|
|
117
|
+
duration: track.duration,
|
|
118
|
+
thumbnail: track.artwork_url,
|
|
119
|
+
requestedBy,
|
|
120
|
+
source: this.name,
|
|
121
|
+
metadata: {
|
|
122
|
+
author: track.user?.username,
|
|
123
|
+
plays: track.playback_count,
|
|
124
|
+
},
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
return { tracks };
|
|
128
|
+
} catch (error: any) {
|
|
129
|
+
throw new Error(`SoundCloud search failed: ${error?.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async getStream(track: Track): Promise<StreamInfo> {
|
|
134
|
+
await this.ready;
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const stream = await this.client.downloadTrack(track.url);
|
|
138
|
+
if (!stream) {
|
|
139
|
+
throw new Error("SoundCloud download returned null");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
stream,
|
|
144
|
+
type: "arbitrary",
|
|
145
|
+
metadata: track.metadata,
|
|
146
|
+
};
|
|
147
|
+
} catch (error: any) {
|
|
148
|
+
throw new Error(`Failed to get SoundCloud stream: ${error.message}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async getRelatedTracks(
|
|
153
|
+
trackURL: string | number,
|
|
154
|
+
opts: { limit?: number; offset?: number; history?: Track[] } = {},
|
|
155
|
+
): Promise<Track[]> {
|
|
156
|
+
await this.ready;
|
|
157
|
+
try {
|
|
158
|
+
const tracks = await this.client.getRelatedTracks(trackURL, {
|
|
159
|
+
limit: 30,
|
|
160
|
+
filter: "tracks",
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!tracks || !tracks?.length) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const relatedfilter = tracks.filter((tr: any) => !(opts?.history ?? []).some((t) => t.url === tr.permalink_url));
|
|
167
|
+
|
|
168
|
+
const related = relatedfilter.slice(0, opts.limit || 1);
|
|
169
|
+
|
|
170
|
+
return related.map((t: any) => ({
|
|
171
|
+
id: t.id.toString(),
|
|
172
|
+
title: t.title,
|
|
173
|
+
url: t.permalink_url,
|
|
174
|
+
duration: t.duration,
|
|
175
|
+
thumbnail: t.artwork_url,
|
|
176
|
+
requestedBy: "auto",
|
|
177
|
+
source: this.name,
|
|
178
|
+
metadata: {
|
|
179
|
+
author: t.user?.username,
|
|
180
|
+
plays: t.playback_count,
|
|
181
|
+
},
|
|
182
|
+
}));
|
|
183
|
+
} catch {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async getFallback(track: Track): Promise<StreamInfo> {
|
|
189
|
+
const trackfall = await this.search(track.title, track.requestedBy);
|
|
190
|
+
const fallbackTrack = trackfall.tracks?.[0];
|
|
191
|
+
if (!fallbackTrack) {
|
|
192
|
+
throw new Error(`No fallback track found for ${track.title}`);
|
|
193
|
+
}
|
|
194
|
+
return await this.getStream(fallbackTrack);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async extractPlaylist(url: string, requestedBy: string): Promise<Track[]> {
|
|
198
|
+
await this.ready;
|
|
199
|
+
try {
|
|
200
|
+
const playlist = await this.client.getPlaylistDetails(url);
|
|
201
|
+
return playlist.tracks.map((t: any) => ({
|
|
202
|
+
id: t.id.toString(),
|
|
203
|
+
title: t.title,
|
|
204
|
+
url: t.permalink_url,
|
|
205
|
+
duration: t.duration,
|
|
206
|
+
thumbnail: t.artwork_url || playlist.artwork_url,
|
|
207
|
+
requestedBy,
|
|
208
|
+
source: this.name,
|
|
209
|
+
metadata: {
|
|
210
|
+
author: t.user?.username,
|
|
211
|
+
plays: t.playback_count,
|
|
212
|
+
playlist: playlist.id?.toString(),
|
|
213
|
+
},
|
|
214
|
+
}));
|
|
215
|
+
} catch {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|