ziplayer 0.2.4 → 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.
- package/README.md +34 -66
- package/dist/extensions/index.d.ts +1 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +5 -5
- package/dist/extensions/index.js.map +1 -1
- package/dist/plugins/BasePlugin.d.ts +4 -3
- package/dist/plugins/BasePlugin.d.ts.map +1 -1
- package/dist/plugins/BasePlugin.js +4 -1
- package/dist/plugins/BasePlugin.js.map +1 -1
- package/dist/plugins/index.d.ts +10 -1
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +159 -38
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/Player.d.ts +0 -9
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +42 -91
- package/dist/structures/Player.js.map +1 -1
- package/dist/types/plugin.d.ts +3 -1
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/plugin.js.map +1 -1
- package/package.json +1 -1
- package/src/extensions/index.ts +5 -9
- package/src/plugins/BasePlugin.ts +4 -3
- package/src/plugins/index.ts +175 -41
- package/src/structures/Player.ts +39 -93
- package/src/types/plugin.ts +3 -1
- package/tsconfig.json +2 -2
package/src/plugins/index.ts
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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,48 +54,183 @@ export class PluginManager {
|
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
async getStream(track: Track): Promise<StreamInfo | null> {
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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;
|
|
125
|
+
}
|
|
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();
|
|
74
135
|
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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;
|
|
93
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);
|
|
94
174
|
}
|
|
95
|
-
|
|
96
|
-
|
|
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();
|
|
97
230
|
}
|
|
98
231
|
}
|
|
99
232
|
|
|
100
|
-
|
|
233
|
+
this.debug(`[RelatedTracks] All plugins failed for: ${track.title}`);
|
|
234
|
+
return [];
|
|
101
235
|
}
|
|
102
236
|
}
|
package/src/structures/Player.ts
CHANGED
|
@@ -165,18 +165,15 @@ export class Player extends EventEmitter {
|
|
|
165
165
|
* @private
|
|
166
166
|
*/
|
|
167
167
|
private destroyCurrentStream(): void {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
if (
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
727
|
-
|
|
728
|
-
|
|
674
|
+
if (this.options.leaveOnEnd) {
|
|
675
|
+
this.scheduleLeave();
|
|
676
|
+
}
|
|
729
677
|
|
|
730
|
-
|
|
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
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
|
package/src/types/plugin.ts
CHANGED
|
@@ -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:
|
|
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
|
}
|