ziplayer 0.2.5 → 0.2.6

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.
@@ -3,7 +3,6 @@ import { withTimeout } from "../utils/timeout";
3
3
  import type { Track, StreamInfo } from "../types";
4
4
  import type { PlayerManager } from "../structures/PlayerManager";
5
5
  import type { Player } from "../structures/Player";
6
- type DebugFn = (message?: any, ...optionalParams: any[]) => void;
7
6
 
8
7
  type PluginManagerOptions = {
9
8
  extractorTimeout: number | undefined;
@@ -13,7 +12,6 @@ export { BasePlugin } from "./BasePlugin";
13
12
 
14
13
  // Plugin factory
15
14
  export class PluginManager {
16
- private debug: DebugFn;
17
15
  private options: PluginManagerOptions;
18
16
  private player: Player;
19
17
  private manager: PlayerManager;
@@ -23,11 +21,12 @@ export class PluginManager {
23
21
  this.player = player;
24
22
  this.manager = manager;
25
23
  this.options = options;
26
- this.debug = (message?: any, ...optionalParams: any[]) => {
27
- if (manager.debugEnabled) {
28
- manager.emit("debug", `[ExtensionManager] ${message}`, ...optionalParams);
29
- }
30
- };
24
+ }
25
+
26
+ debug(message?: any, ...optionalParams: any[]): void {
27
+ if (this.manager.debugEnabled) {
28
+ this.manager.emit("debug", `[Plugins] ${message}`, ...optionalParams);
29
+ }
31
30
  }
32
31
 
33
32
  register(plugin: BasePlugin): void {
@@ -55,55 +54,183 @@ export class PluginManager {
55
54
  }
56
55
 
57
56
  async getStream(track: Track): Promise<StreamInfo | null> {
58
- let streamInfo: StreamInfo | null = null;
59
- const plugin = this.get(track.source) || this.findPlugin(track.url);
60
-
61
- if (!plugin) {
62
- this.debug(`[Player] No plugin found for track: ${track.title}`);
57
+ const timeoutMs = this.options.extractorTimeout ?? 50000;
58
+ const primary = this.get(track.source) || this.findPlugin(track.url);
59
+ if (!primary) {
60
+ this.debug(`No plugin found for track: ${track.title}`);
63
61
  return null;
64
62
  }
65
-
66
- this.debug(`[Player] Getting stream for track: ${track.title}`);
67
- this.debug(`[Player] Using plugin: ${plugin.name}`);
68
- this.debug(`[Track] Track Info:`, track);
69
- const timeoutMs = this.options.extractorTimeout ?? 50000;
70
63
  try {
71
- streamInfo = await withTimeout(plugin.getStream(track), timeoutMs, "getStream timed out");
72
- if (!(streamInfo as any)?.stream) {
73
- throw new Error(`No stream returned from ${plugin.name}`);
74
- }
75
- } catch (streamError) {
76
- this.debug(`[Player] getStream failed, trying getFallback:`, streamError);
77
- const allplugs = this.getAll();
78
- for (const p of allplugs) {
79
- try {
80
- if (typeof p.getStream == "function") {
81
- streamInfo = await withTimeout((p as any).getStream(track), timeoutMs, `getStream timed out for plugin ${p.name}`);
82
- if ((streamInfo as any)?.stream) {
83
- this.debug(`[Player] getStream succeeded with plugin ${p.name} for track: ${track.title}`);
84
- return streamInfo as StreamInfo;
85
- }
86
- }
87
- if (typeof p.getFallback == "function") {
88
- streamInfo = await withTimeout(
89
- (p as any).getFallback(track),
90
- timeoutMs,
91
- `getFallback timed out for plugin ${p.name}`,
92
- );
93
- if ((streamInfo as any)?.stream) {
94
- this.debug(`[Player] getFallback succeeded with plugin ${p.name} for track: ${track.title}`);
95
- return streamInfo as StreamInfo;
64
+ const controller = new AbortController();
65
+ const result = await withTimeout(primary.getStream(track, controller.signal), timeoutMs, "Primary timeout");
66
+ if (result?.stream) return result;
67
+ throw new Error("Primary failed");
68
+ } catch {
69
+ this.debug("Primary failed fallback parallel");
70
+ }
71
+
72
+ // ===== FALLBACK PARALLEL =====
73
+ const plugins = this.getAll()
74
+ .filter((p) => p !== primary)
75
+ .map((p) => {
76
+ p.priority ??= 0;
77
+ return p;
78
+ })
79
+ .sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
80
+
81
+ // group by priority
82
+ const groups = new Map<number, BasePlugin[]>();
83
+ for (const p of plugins) {
84
+ if (!groups.has(p.priority ?? 0)) groups.set(p.priority ?? 0, []);
85
+ groups.get(p.priority ?? 0)!.push(p);
86
+ }
87
+ for (const [priority, group] of groups) {
88
+ this.debug(`Running group priority=${priority}`);
89
+ const controller = new AbortController();
90
+ try {
91
+ const promises = group.map((p) => {
92
+ const run = async () => {
93
+ try {
94
+ let result: StreamInfo | null = null;
95
+
96
+ if (p.getStream) {
97
+ try {
98
+ result = await withTimeout(p.getStream(track, controller.signal), timeoutMs, `Timeout ${p.name}`);
99
+ } catch (err) {
100
+ // getStream thất bại → log rồi thử getFallback
101
+ this.debug(`getStream failed for ${p.name}, trying getFallback`, err);
102
+ }
103
+
104
+ if (result?.stream) {
105
+ this.debug(`Success via ${p.name}`);
106
+ controller.abort();
107
+ return result;
108
+ }
109
+ }
110
+
111
+ if (p.getFallback) {
112
+ result = await withTimeout(p.getFallback(track, controller.signal), timeoutMs, `Fallback timeout ${p.name}`);
113
+ if (result?.stream) {
114
+ this.debug(`Fallback via ${p.name}`);
115
+ controller.abort();
116
+ return result;
117
+ }
118
+ }
119
+
120
+ throw new Error("No stream");
121
+ } catch (err) {
122
+ if (controller.signal.aborted) throw new Error("Aborted");
123
+ this.debug(`Failed ${p.name}`, err);
124
+ throw err;
96
125
  }
97
- }
98
- } catch (fallbackError) {
99
- this.debug(`[Player] getFallback failed with plugin ${p.name}:`, fallbackError);
126
+ };
127
+ return run();
128
+ });
129
+
130
+ const result = await Promise.any(promises);
131
+ if (result?.stream) return result;
132
+ } catch {
133
+ this.debug(`Priority group ${priority} failed`);
134
+ controller.abort();
135
+ }
136
+ }
137
+
138
+ throw new Error(`All plugins failed for track: ${track.title}`);
139
+ }
140
+
141
+ /**
142
+ * Get related tracks for a given track
143
+ * @param {Track} track Track to find related tracks for
144
+ * @returns {Track[]} Related tracks or empty array
145
+ * @example
146
+ * const related = await player.getRelatedTracks(track);
147
+ * console.log(`Found ${related.length} related tracks`);
148
+ */
149
+ async getRelatedTracks(track: Track): Promise<Track[]> {
150
+ if (!track) return [];
151
+
152
+ const timeoutMs = this.options.extractorTimeout ?? 15000;
153
+ const preferred = this.findPlugin(track.url) || this.get(track.source);
154
+
155
+ // ===== THỬ PREFERRED TRƯỚC =====
156
+ if (preferred && typeof preferred.getRelatedTracks === "function") {
157
+ try {
158
+ this.debug(`[RelatedTracks] Trying preferred: ${preferred.name}`);
159
+ const related = await withTimeout(
160
+ preferred.getRelatedTracks(track, {
161
+ limit: 10,
162
+ history: this.player.queue.previousTracks,
163
+ }),
164
+ timeoutMs,
165
+ `getRelatedTracks timed out for ${preferred.name}`,
166
+ );
167
+
168
+ if (Array.isArray(related) && related.length > 0) {
169
+ return related;
100
170
  }
171
+ this.debug(`[RelatedTracks] ${preferred.name} returned no results → fallback race`);
172
+ } catch (err) {
173
+ this.debug(`[RelatedTracks] ${preferred.name} failed → fallback race`, err);
101
174
  }
102
- if (!(streamInfo as any)?.stream) {
103
- throw new Error(`All getFallback attempts failed for track: ${track.title}`);
175
+ }
176
+
177
+ // ===== FALLBACK: RACE THEO PRIORITY GROUP =====
178
+ const plugins = this.getAll()
179
+ .filter((p) => p !== preferred && typeof p.getRelatedTracks === "function")
180
+ .map((p) => {
181
+ p.priority ??= 0;
182
+ return p;
183
+ })
184
+ .sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
185
+
186
+ // group by priority
187
+ const groups = new Map<number, BasePlugin[]>();
188
+ for (const p of plugins) {
189
+ const key = p.priority ?? 0;
190
+ if (!groups.has(key)) groups.set(key, []);
191
+ groups.get(key)!.push(p);
192
+ }
193
+
194
+ for (const [priority, group] of groups) {
195
+ this.debug(`[RelatedTracks] Racing priority=${priority} (${group.map((p) => p.name).join(", ")})`);
196
+ const controller = new AbortController();
197
+
198
+ try {
199
+ const promises = group.map((p) =>
200
+ (async () => {
201
+ try {
202
+ const related = await withTimeout(
203
+ p.getRelatedTracks!(track, {
204
+ limit: 10,
205
+ history: this.player.queue.previousTracks,
206
+ }),
207
+ timeoutMs,
208
+ `getRelatedTracks timed out for ${p.name}`,
209
+ );
210
+
211
+ if (Array.isArray(related) && related.length > 0) {
212
+ this.debug(`[RelatedTracks] Success via ${p.name}`);
213
+ controller.abort();
214
+ return related;
215
+ }
216
+ throw new Error(`${p.name} returned no results`);
217
+ } catch (err) {
218
+ if (controller.signal.aborted) throw new Error("Aborted");
219
+ this.debug(`[RelatedTracks] ${p.name} failed`, err);
220
+ throw err;
221
+ }
222
+ })(),
223
+ );
224
+
225
+ const result = await Promise.any(promises);
226
+ if (result) return result;
227
+ } catch {
228
+ this.debug(`[RelatedTracks] Priority group ${priority} all failed`);
229
+ controller.abort();
104
230
  }
105
231
  }
106
232
 
107
- return streamInfo as StreamInfo;
233
+ this.debug(`[RelatedTracks] All plugins failed for: ${track.title}`);
234
+ return [];
108
235
  }
109
236
  }
@@ -165,18 +165,15 @@ export class Player extends EventEmitter {
165
165
  * @private
166
166
  */
167
167
  private destroyCurrentStream(): void {
168
- try {
169
- // Get the metadata from current resource to find the stream
170
- if (this.currentResource) {
171
- const stream = (this.currentResource as any)?.metadata?.stream || (this.currentResource as any)?.stream;
172
- if (stream && typeof stream.destroy === "function") {
173
- stream.destroy();
174
- this.debug(`[Player] Destroyed current stream`);
175
- }
176
- }
177
- } catch (error) {
178
- this.debug(`[Player] Error destroying current stream:`, error);
168
+ if (!this.currentResource) return;
169
+
170
+ const stream = (this.currentResource as any)?.metadata?.stream ?? (this.currentResource as any)?.stream;
171
+
172
+ if (stream?.destroy) {
173
+ stream.destroy();
179
174
  }
175
+
176
+ this.currentResource = null;
180
177
  }
181
178
 
182
179
  //#region Search
@@ -354,54 +351,10 @@ export class Player extends EventEmitter {
354
351
  };
355
352
  }
356
353
 
357
- /**
358
- * Get related tracks for a given track
359
- * @param {Track} track Track to find related tracks for
360
- * @returns {Track[]} Related tracks or empty array
361
- * @example
362
- * const related = await player.getRelatedTracks(track);
363
- * console.log(`Found ${related.length} related tracks`);
364
- */
365
- async getRelatedTracks(track: Track): Promise<Track[]> {
366
- if (!track) return [];
367
-
368
- const preferred = this.pluginManager.findPlugin(track.url) || this.pluginManager.get(track.source);
369
-
370
- const all = this.pluginManager.getAll();
371
-
372
- const candidates = [...(preferred ? [preferred] : []), ...all.filter((p) => p !== preferred)].filter(
373
- (p) => typeof (p as any).getRelatedTracks === "function",
374
- );
375
-
376
- for (const p of candidates) {
377
- try {
378
- this.debug(`[Player] Trying related from plugin: ${p.name}`);
379
- const related = await withTimeout(
380
- (p as any).getRelatedTracks(track.url, {
381
- limit: 10,
382
- history: this.queue.previousTracks,
383
- }),
384
- this.options.extractorTimeout ?? 15000,
385
- `getRelatedTracks timed out for ${p.name}`,
386
- );
387
-
388
- if (Array.isArray(related) && related.length > 0) {
389
- return related; // success
390
- }
391
- this.debug(`[Player] ${p.name} returned no related tracks`);
392
- } catch (err) {
393
- this.debug(`[Player] getRelatedTracks error from ${p.name}:`, err);
394
- return [];
395
- // try next candidate
396
- }
397
- }
398
- return [];
399
- }
400
-
401
354
  private async generateWillNext(): Promise<void> {
402
355
  const lastTrack = this.queue.previousTracks[this.queue.previousTracks.length - 1] ?? this.queue.currentTrack;
403
356
  if (!lastTrack) return;
404
- const related = await this.getRelatedTracks(lastTrack);
357
+ const related = await this.pluginManager.getRelatedTracks(lastTrack);
405
358
  if (!related || related.length === 0) return;
406
359
  const randomchoice = Math.floor(Math.random() * related.length);
407
360
  const nextTrack = this.queue.nextTrack ? this.queue.nextTrack : related[randomchoice];
@@ -646,15 +599,6 @@ export class Player extends EventEmitter {
646
599
  try {
647
600
  // Destroy the old stream and resource before creating a new one
648
601
  this.destroyCurrentStream();
649
- if (this.currentResource) {
650
- try {
651
- const oldStream = (this.currentResource as any)._readableState?.stream || (this.currentResource as any).stream;
652
- if (oldStream && typeof oldStream.destroy === "function") {
653
- oldStream.destroy();
654
- }
655
- } catch {}
656
- this.currentResource = null;
657
- }
658
602
 
659
603
  this.currentResource = await this.createResource(streamInfo, track, 0);
660
604
  if (this.volumeInterval) {
@@ -710,39 +654,41 @@ export class Player extends EventEmitter {
710
654
  }
711
655
 
712
656
  private async playNext(): Promise<boolean> {
713
- this.debug(`[Player] playNext called`);
714
- const track = this.queue.next(this.skipLoop);
715
- this.skipLoop = false;
716
- if (!track) {
717
- if (this.queue.autoPlay()) {
718
- const willnext = this.queue.willNextTrack();
719
- if (willnext) {
720
- this.debug(`[Player] Auto-playing next track: ${willnext.title}`);
721
- this.queue.addMultiple([willnext]);
722
- return this.playNext();
657
+ this.debug(`[Player] playNext called by ${new Error().stack?.split("\n")[2]?.trim()}`);
658
+ while (true) {
659
+ const track = this.queue.next(this.skipLoop);
660
+ this.skipLoop = false;
661
+
662
+ if (!track) {
663
+ if (this.queue.autoPlay()) {
664
+ const willnext = this.queue.willNextTrack();
665
+ if (willnext) {
666
+ this.queue.addMultiple([willnext]);
667
+ continue;
668
+ }
723
669
  }
724
- }
670
+ this.debug(`[Player] No next track in queue`);
671
+ this.isPlaying = false;
672
+ this.emit("queueEnd");
725
673
 
726
- this.debug(`[Player] No next track in queue`);
727
- this.isPlaying = false;
728
- this.emit("queueEnd");
674
+ if (this.options.leaveOnEnd) {
675
+ this.scheduleLeave();
676
+ }
729
677
 
730
- if (this.options.leaveOnEnd) {
731
- this.scheduleLeave();
678
+ return false;
732
679
  }
733
- return false;
734
- }
735
-
736
- this.generateWillNext();
737
- // A new track is about to play; ensure we don't leave mid-playback
738
- this.clearLeaveTimeout();
739
680
 
740
- try {
741
- return await this.startTrack(track);
742
- } catch (error) {
743
- this.debug(`[Player] playNext error:`, error);
744
- this.emit("playerError", error as Error, track);
745
- return this.playNext();
681
+ this.generateWillNext();
682
+ this.clearLeaveTimeout();
683
+ this.debug(`[Player] playNext called for track: ${track.title}`);
684
+
685
+ try {
686
+ return await this.startTrack(track);
687
+ } catch (err) {
688
+ this.debug(`[Player] playNext error:`, err);
689
+ this.emit("playerError", err as Error, track);
690
+ continue;
691
+ }
746
692
  }
747
693
  }
748
694
 
@@ -6,15 +6,17 @@ import type { SearchResult, StreamInfo, Track } from ".";
6
6
  * const plugin: SourcePlugin = {
7
7
  * name: "YouTube",
8
8
  * version: "1.0.0"
9
+ * priority: 0, // Optional, default is 0. Lower priority plugins are tried first in getStream fallback.
9
10
  * };
10
11
  */
11
12
  export interface SourcePlugin {
12
13
  name: string;
13
14
  version: string;
15
+ priority?: number;
14
16
  canHandle(query: string): boolean;
15
17
  search(query: string, requestedBy: string): Promise<SearchResult>;
16
18
  getStream(track: Track): Promise<StreamInfo>;
17
- getRelatedTracks?(track: string | number, opts?: { limit?: number; offset?: number }): Promise<Track[]>;
19
+ getRelatedTracks?(track: Track, opts?: { limit?: number; offset?: number }): Promise<Track[]>;
18
20
  validate?(url: string): boolean;
19
21
  extractPlaylist?(url: string, requestedBy: string): Promise<Track[]>;
20
22
  }
package/tsconfig.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "target": "ES2020",
3
+ "target": "es2021",
4
4
  "module": "commonjs",
5
- "lib": ["ES2020"],
5
+ "lib": ["ES2021"],
6
6
  "outDir": "./dist",
7
7
  "rootDir": "./src",
8
8
  "strict": true,