@ziplayer/plugin 0.0.1 → 0.0.3

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.
@@ -1,191 +1,207 @@
1
- import { BasePlugin, Track, SearchResult, StreamInfo } from "ziplayer";
2
-
3
- const SoundCloud = require("@zibot/scdl");
4
-
5
- export class SoundCloudPlugin extends BasePlugin {
6
- name = "soundcloud";
7
- version = "1.0.0";
8
- private client: any;
9
- private ready: Promise<void>;
10
-
11
- constructor() {
12
- super();
13
- this.ready = this.init();
14
- }
15
-
16
- private async init(): Promise<void> {
17
- this.client = new SoundCloud({ init: false });
18
- await this.client.init();
19
- }
20
-
21
- canHandle(query: string): boolean {
22
- return (
23
- query.includes("soundcloud.com") ||
24
- (!query.startsWith("http") && !query.includes("youtube"))
25
- );
26
- }
27
-
28
- validate(url: string): boolean {
29
- return url.includes("soundcloud.com");
30
- }
31
-
32
- async search(query: string, requestedBy: string): Promise<SearchResult> {
33
- await this.ready;
34
-
35
- try {
36
- if (query.includes("soundcloud.com")) {
37
- try {
38
- const info = await this.client.getTrackDetails(query);
39
- const track: Track = {
40
- id: info.id.toString(),
41
- title: info.title,
42
- url: info.permalink_url || query,
43
- duration: info.duration,
44
- thumbnail: info.artwork_url,
45
- requestedBy,
46
- source: this.name,
47
- metadata: {
48
- author: info.user?.username,
49
- plays: info.playback_count,
50
- },
51
- };
52
- return { tracks: [track] };
53
- } catch {
54
- const playlist = await this.client.getPlaylistDetails(query);
55
- const tracks: Track[] = playlist.tracks.map((t: any) => ({
56
- id: t.id.toString(),
57
- title: t.title,
58
- url: t.permalink_url,
59
- duration: t.duration,
60
- thumbnail: t.artwork_url || playlist.artwork_url,
61
- requestedBy,
62
- source: this.name,
63
- metadata: {
64
- author: t.user?.username,
65
- plays: t.playback_count,
66
- playlist: playlist.id?.toString(),
67
- },
68
- }));
69
-
70
- return {
71
- tracks,
72
- playlist: {
73
- name: playlist.title,
74
- url: playlist.permalink_url || query,
75
- thumbnail: playlist.artwork_url,
76
- },
77
- };
78
- }
79
- }
80
-
81
- const results = await this.client.searchTracks({ query, limit: 15 });
82
- const tracks: Track[] = results.slice(0, 10).map((track: any) => ({
83
- id: track.id.toString(),
84
- title: track.title,
85
- url: track.permalink_url,
86
- duration: track.duration,
87
- thumbnail: track.artwork_url,
88
- requestedBy,
89
- source: this.name,
90
- metadata: {
91
- author: track.user?.username,
92
- plays: track.playback_count,
93
- },
94
- }));
95
-
96
- return { tracks };
97
- } catch (error: any) {
98
- throw new Error(`SoundCloud search failed: ${error?.message}`);
99
- }
100
- }
101
-
102
- async getStream(track: Track): Promise<StreamInfo> {
103
- await this.ready;
104
-
105
- try {
106
- const stream = await this.client.downloadTrack(track.url);
107
- if (!stream) {
108
- throw new Error("SoundCloud download returned null");
109
- }
110
-
111
- return {
112
- stream,
113
- type: "arbitrary",
114
- metadata: track.metadata,
115
- };
116
- } catch (error: any) {
117
- throw new Error(`Failed to get SoundCloud stream: ${error.message}`);
118
- }
119
- }
120
-
121
- async getRelatedTracks(
122
- trackURL: string | number,
123
- opts: { limit?: number; offset?: number; history?: Track[] } = {}
124
- ): Promise<Track[]> {
125
- await this.ready;
126
- try {
127
- const tracks = await this.client.getRelatedTracks(trackURL, {
128
- limit: 30,
129
- filter: "tracks",
130
- });
131
-
132
- if (!tracks || !tracks?.length) {
133
- return [];
134
- }
135
- const relatedfilter = tracks.filter(
136
- (tr: any) =>
137
- !(opts?.history ?? []).some((t) => t.url === tr.permalink_url)
138
- );
139
-
140
- const related = relatedfilter.slice(0, opts.limit || 1);
141
-
142
- return related.map((t: any) => ({
143
- id: t.id.toString(),
144
- title: t.title,
145
- url: t.permalink_url,
146
- duration: t.duration,
147
- thumbnail: t.artwork_url,
148
- requestedBy: "auto",
149
- source: this.name,
150
- metadata: {
151
- author: t.user?.username,
152
- plays: t.playback_count,
153
- },
154
- }));
155
- } catch {
156
- return [];
157
- }
158
- }
159
-
160
- async getFallback(track: Track): Promise<StreamInfo> {
161
- const trackfall = await this.search(track.title, track.requestedBy);
162
- const fallbackTrack = trackfall.tracks?.[0];
163
- if (!fallbackTrack) {
164
- throw new Error(`No fallback track found for ${track.title}`);
165
- }
166
- return await this.getStream(fallbackTrack);
167
- }
168
-
169
- async extractPlaylist(url: string, requestedBy: string): Promise<Track[]> {
170
- await this.ready;
171
- try {
172
- const playlist = await this.client.getPlaylistDetails(url);
173
- return playlist.tracks.map((t: any) => ({
174
- id: t.id.toString(),
175
- title: t.title,
176
- url: t.permalink_url,
177
- duration: t.duration,
178
- thumbnail: t.artwork_url || playlist.artwork_url,
179
- requestedBy,
180
- source: this.name,
181
- metadata: {
182
- author: t.user?.username,
183
- plays: t.playback_count,
184
- playlist: playlist.id?.toString(),
185
- },
186
- }));
187
- } catch {
188
- return [];
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"];
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
+ // Handle only SoundCloud URLs directly
36
+ if (q.startsWith("http")) {
37
+ return isValidSoundCloudHost(query);
38
+ }
39
+ // Avoid intercepting explicit patterns for other extractors (e.g., TTS)
40
+ if (q.startsWith("tts:") || q.startsWith("say ")) return false;
41
+ // Heuristic: prefer SoundCloud for generic text when it mentions soundcloud
42
+ if (q.includes("soundcloud")) return true;
43
+ // Otherwise, do not greedily claim generic queries
44
+ return false;
45
+ }
46
+
47
+ validate(url: string): boolean {
48
+ return isValidSoundCloudHost(url);
49
+ }
50
+
51
+ async search(query: string, requestedBy: string): Promise<SearchResult> {
52
+ await this.ready;
53
+
54
+ try {
55
+ if (isValidSoundCloudHost(query)) {
56
+ try {
57
+ const info = await this.client.getTrackDetails(query);
58
+ const track: Track = {
59
+ id: info.id.toString(),
60
+ title: info.title,
61
+ url: info.permalink_url || query,
62
+ duration: info.duration,
63
+ thumbnail: info.artwork_url,
64
+ requestedBy,
65
+ source: this.name,
66
+ metadata: {
67
+ author: info.user?.username,
68
+ plays: info.playback_count,
69
+ },
70
+ };
71
+ return { tracks: [track] };
72
+ } catch {
73
+ const playlist = await this.client.getPlaylistDetails(query);
74
+ const tracks: Track[] = playlist.tracks.map((t: any) => ({
75
+ id: t.id.toString(),
76
+ title: t.title,
77
+ url: t.permalink_url,
78
+ duration: t.duration,
79
+ thumbnail: t.artwork_url || playlist.artwork_url,
80
+ requestedBy,
81
+ source: this.name,
82
+ metadata: {
83
+ author: t.user?.username,
84
+ plays: t.playback_count,
85
+ playlist: playlist.id?.toString(),
86
+ },
87
+ }));
88
+
89
+ return {
90
+ tracks,
91
+ playlist: {
92
+ name: playlist.title,
93
+ url: playlist.permalink_url || query,
94
+ thumbnail: playlist.artwork_url,
95
+ },
96
+ };
97
+ }
98
+ }
99
+
100
+ const results = await this.client.searchTracks({ query, limit: 15 });
101
+ const tracks: Track[] = results.slice(0, 10).map((track: any) => ({
102
+ id: track.id.toString(),
103
+ title: track.title,
104
+ url: track.permalink_url,
105
+ duration: track.duration,
106
+ thumbnail: track.artwork_url,
107
+ requestedBy,
108
+ source: this.name,
109
+ metadata: {
110
+ author: track.user?.username,
111
+ plays: track.playback_count,
112
+ },
113
+ }));
114
+
115
+ return { tracks };
116
+ } catch (error: any) {
117
+ throw new Error(`SoundCloud search failed: ${error?.message}`);
118
+ }
119
+ }
120
+
121
+ async getStream(track: Track): Promise<StreamInfo> {
122
+ await this.ready;
123
+
124
+ try {
125
+ const stream = await this.client.downloadTrack(track.url);
126
+ if (!stream) {
127
+ throw new Error("SoundCloud download returned null");
128
+ }
129
+
130
+ return {
131
+ stream,
132
+ type: "arbitrary",
133
+ metadata: track.metadata,
134
+ };
135
+ } catch (error: any) {
136
+ throw new Error(`Failed to get SoundCloud stream: ${error.message}`);
137
+ }
138
+ }
139
+
140
+ async getRelatedTracks(
141
+ trackURL: string | number,
142
+ opts: { limit?: number; offset?: number; history?: Track[] } = {},
143
+ ): Promise<Track[]> {
144
+ await this.ready;
145
+ try {
146
+ const tracks = await this.client.getRelatedTracks(trackURL, {
147
+ limit: 30,
148
+ filter: "tracks",
149
+ });
150
+
151
+ if (!tracks || !tracks?.length) {
152
+ return [];
153
+ }
154
+ const relatedfilter = tracks.filter((tr: any) => !(opts?.history ?? []).some((t) => t.url === tr.permalink_url));
155
+
156
+ const related = relatedfilter.slice(0, opts.limit || 1);
157
+
158
+ return related.map((t: any) => ({
159
+ id: t.id.toString(),
160
+ title: t.title,
161
+ url: t.permalink_url,
162
+ duration: t.duration,
163
+ thumbnail: t.artwork_url,
164
+ requestedBy: "auto",
165
+ source: this.name,
166
+ metadata: {
167
+ author: t.user?.username,
168
+ plays: t.playback_count,
169
+ },
170
+ }));
171
+ } catch {
172
+ return [];
173
+ }
174
+ }
175
+
176
+ async getFallback(track: Track): Promise<StreamInfo> {
177
+ const trackfall = await this.search(track.title, track.requestedBy);
178
+ const fallbackTrack = trackfall.tracks?.[0];
179
+ if (!fallbackTrack) {
180
+ throw new Error(`No fallback track found for ${track.title}`);
181
+ }
182
+ return await this.getStream(fallbackTrack);
183
+ }
184
+
185
+ async extractPlaylist(url: string, requestedBy: string): Promise<Track[]> {
186
+ await this.ready;
187
+ try {
188
+ const playlist = await this.client.getPlaylistDetails(url);
189
+ return playlist.tracks.map((t: any) => ({
190
+ id: t.id.toString(),
191
+ title: t.title,
192
+ url: t.permalink_url,
193
+ duration: t.duration,
194
+ thumbnail: t.artwork_url || playlist.artwork_url,
195
+ requestedBy,
196
+ source: this.name,
197
+ metadata: {
198
+ author: t.user?.username,
199
+ plays: t.playback_count,
200
+ playlist: playlist.id?.toString(),
201
+ },
202
+ }));
203
+ } catch {
204
+ return [];
205
+ }
206
+ }
207
+ }