ziplayer 0.3.2 → 0.3.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/{AI-Guide.md → AGENTS.md} +717 -624
- package/README.md +658 -526
- package/dist/extensions/BaseExtension.d.ts +10 -1
- package/dist/extensions/BaseExtension.d.ts.map +1 -1
- package/dist/extensions/BaseExtension.js +27 -1
- package/dist/extensions/BaseExtension.js.map +1 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +24 -6
- package/dist/extensions/index.js.map +1 -1
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +105 -51
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/Player.d.ts +90 -15
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +487 -81
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +70 -6
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +184 -19
- package/dist/structures/PlayerManager.js.map +1 -1
- package/dist/structures/Queue.d.ts +19 -0
- package/dist/structures/Queue.d.ts.map +1 -1
- package/dist/structures/Queue.js +21 -0
- package/dist/structures/Queue.js.map +1 -1
- package/dist/types/extension.d.ts +3 -0
- package/dist/types/extension.d.ts.map +1 -1
- package/dist/types/index.d.ts +69 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +13 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
- package/src/extensions/BaseExtension.ts +31 -1
- package/src/extensions/index.ts +30 -7
- package/src/plugins/index.ts +137 -54
- package/src/structures/Player.ts +2937 -2457
- package/src/structures/PlayerManager.ts +916 -725
- package/src/structures/Queue.ts +621 -599
- package/src/types/extension.ts +3 -0
- package/src/types/index.ts +80 -2
package/src/structures/Queue.ts
CHANGED
|
@@ -1,599 +1,621 @@
|
|
|
1
|
-
import { Track, LoopMode } from "../types";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Manages the track queue for a player.
|
|
5
|
-
*
|
|
6
|
-
* @example
|
|
7
|
-
* // Basic queue operations
|
|
8
|
-
* const queue = player.queue;
|
|
9
|
-
*
|
|
10
|
-
* // Add single track
|
|
11
|
-
* queue.add(track);
|
|
12
|
-
*
|
|
13
|
-
* // Add multiple tracks
|
|
14
|
-
* queue.add([track1, track2, track3]);
|
|
15
|
-
*
|
|
16
|
-
* // Queue controls
|
|
17
|
-
* queue.shuffle(); // Randomize order
|
|
18
|
-
* queue.clear(); // Remove all tracks
|
|
19
|
-
* queue.autoPlay(true); // Enable auto-play
|
|
20
|
-
*
|
|
21
|
-
* // Get queue information
|
|
22
|
-
* console.log(`Queue length: ${queue.length}`);
|
|
23
|
-
* console.log(`Current track: ${queue.current?.title}`);
|
|
24
|
-
* console.log(`Is empty: ${queue.isEmpty}`);
|
|
25
|
-
* console.log(`Is playing: ${queue.isPlaying}`);
|
|
26
|
-
*
|
|
27
|
-
* // Loop modes
|
|
28
|
-
* queue.setLoopMode("track"); // Loop current track
|
|
29
|
-
* queue.setLoopMode("queue"); // Loop entire queue
|
|
30
|
-
* queue.setLoopMode("off"); // No loop
|
|
31
|
-
*
|
|
32
|
-
* // Remove specific track
|
|
33
|
-
* const removed = queue.remove(0); // Remove first track
|
|
34
|
-
* if (removed) {
|
|
35
|
-
* console.log(`Removed: ${removed.title}`);
|
|
36
|
-
* }
|
|
37
|
-
*/
|
|
38
|
-
export class Queue {
|
|
39
|
-
private tracks: Track[] = [];
|
|
40
|
-
private current: Track | null = null;
|
|
41
|
-
private history: Track[] = [];
|
|
42
|
-
private related: Track[] = [];
|
|
43
|
-
private _autoPlay = false;
|
|
44
|
-
private _loop: LoopMode = "off";
|
|
45
|
-
private willnext: Track | null = null;
|
|
46
|
-
|
|
47
|
-
// Configuration
|
|
48
|
-
private readonly MAX_HISTORY_SIZE = 200;
|
|
49
|
-
private readonly MAX_QUEUE_SIZE = 1000; // Prevent memory issues
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Add track(s) to the queue
|
|
53
|
-
*
|
|
54
|
-
* @param {Track | Track[]} track - Track or array of tracks to add
|
|
55
|
-
* @returns {number} New queue size
|
|
56
|
-
* @example
|
|
57
|
-
* queue.add(track);
|
|
58
|
-
* queue.add([track1, track2, track3]);
|
|
59
|
-
*/
|
|
60
|
-
add(track: Track): number {
|
|
61
|
-
if (this.tracks.length >= this.MAX_QUEUE_SIZE) {
|
|
62
|
-
throw new Error(`Queue size limit reached (${this.MAX_QUEUE_SIZE})`);
|
|
63
|
-
}
|
|
64
|
-
this.tracks.push(track);
|
|
65
|
-
return this.tracks.length;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Add multiple tracks to the queue
|
|
70
|
-
*
|
|
71
|
-
* @param {Track[]} tracks - Tracks to add
|
|
72
|
-
* @returns {number} New queue size
|
|
73
|
-
* @example
|
|
74
|
-
* queue.addMultiple([track1, track2, track3]);
|
|
75
|
-
*/
|
|
76
|
-
addMultiple(tracks: Track[]): number {
|
|
77
|
-
if (this.tracks.length + tracks.length > this.MAX_QUEUE_SIZE) {
|
|
78
|
-
throw new Error(`Adding ${tracks.length} tracks would exceed queue size limit (${this.MAX_QUEUE_SIZE})`);
|
|
79
|
-
}
|
|
80
|
-
this.tracks.push(...tracks);
|
|
81
|
-
return this.tracks.length;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Insert a track at a specific upcoming position (0 = next)
|
|
86
|
-
*
|
|
87
|
-
* @param {Track} track - Track to insert
|
|
88
|
-
* @param {number} index - Index to insert the track at
|
|
89
|
-
* @returns {number} New queue size
|
|
90
|
-
* @example
|
|
91
|
-
* queue.insert(track, 0);
|
|
92
|
-
*/
|
|
93
|
-
insert(track: Track, index: number): number {
|
|
94
|
-
if (this.tracks.length >= this.MAX_QUEUE_SIZE) {
|
|
95
|
-
throw new Error(`Queue size limit reached (${this.MAX_QUEUE_SIZE})`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!Number.isFinite(index)) {
|
|
99
|
-
this.tracks.push(track);
|
|
100
|
-
return this.tracks.length;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const i = Math.max(0, Math.min(Math.floor(index), this.tracks.length));
|
|
104
|
-
|
|
105
|
-
if (i === this.tracks.length) {
|
|
106
|
-
this.tracks.push(track);
|
|
107
|
-
} else if (i <= 0) {
|
|
108
|
-
this.tracks.unshift(track);
|
|
109
|
-
} else {
|
|
110
|
-
this.tracks.splice(i, 0, track);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return this.tracks.length;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Insert multiple tracks at a specific upcoming position, preserving order
|
|
118
|
-
*
|
|
119
|
-
* @param {Track[]} tracks - Tracks to insert
|
|
120
|
-
* @param {number} index - Index to insert the tracks at
|
|
121
|
-
* @returns {number} New queue size
|
|
122
|
-
* @example
|
|
123
|
-
* queue.insertMultiple([track1, track2, track3], 0);
|
|
124
|
-
*/
|
|
125
|
-
insertMultiple(tracks: Track[], index: number): number {
|
|
126
|
-
if (!Array.isArray(tracks) || tracks.length === 0) return this.tracks.length;
|
|
127
|
-
|
|
128
|
-
if (this.tracks.length + tracks.length > this.MAX_QUEUE_SIZE) {
|
|
129
|
-
throw new Error(`Inserting ${tracks.length} tracks would exceed queue size limit (${this.MAX_QUEUE_SIZE})`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (!Number.isFinite(index)) {
|
|
133
|
-
this.tracks.push(...tracks);
|
|
134
|
-
return this.tracks.length;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const i = Math.max(0, Math.min(Math.floor(index), this.tracks.length));
|
|
138
|
-
|
|
139
|
-
if (i === 0) {
|
|
140
|
-
this.tracks = [...tracks, ...this.tracks];
|
|
141
|
-
} else if (i === this.tracks.length) {
|
|
142
|
-
this.tracks.push(...tracks);
|
|
143
|
-
} else {
|
|
144
|
-
this.tracks.splice(i, 0, ...tracks);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return this.tracks.length;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Remove a track from the queue
|
|
152
|
-
*
|
|
153
|
-
* @param {number} index - Index of track to remove
|
|
154
|
-
* @returns {Track | null} Removed track or null
|
|
155
|
-
* @example
|
|
156
|
-
* const removed = queue.remove(0);
|
|
157
|
-
* console.log(`Removed: ${removed?.title}`);
|
|
158
|
-
*/
|
|
159
|
-
remove(index: number): Track | null {
|
|
160
|
-
if (index < 0 || index >= this.tracks.length) return null;
|
|
161
|
-
const removed = this.tracks.splice(index, 1)[0];
|
|
162
|
-
return removed;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Remove multiple tracks by indices
|
|
167
|
-
*
|
|
168
|
-
* @param {number[]} indices - Array of indices to remove
|
|
169
|
-
* @returns {Track[]} Removed tracks
|
|
170
|
-
* @example
|
|
171
|
-
* const removed = queue.removeMultiple([0, 2, 5]);
|
|
172
|
-
*/
|
|
173
|
-
removeMultiple(indices: number[]): Track[] {
|
|
174
|
-
const sorted = [...new Set(indices)].sort((a, b) => b - a);
|
|
175
|
-
const removed: Track[] = [];
|
|
176
|
-
|
|
177
|
-
for (const index of sorted) {
|
|
178
|
-
if (index >= 0 && index < this.tracks.length) {
|
|
179
|
-
removed.unshift(this.tracks.splice(index, 1)[0]);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return removed;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Remove tracks by predicate
|
|
188
|
-
*
|
|
189
|
-
* @param {(track: Track, index: number) => boolean} predicate - Filter function
|
|
190
|
-
* @returns {Track[]} Removed tracks
|
|
191
|
-
* @example
|
|
192
|
-
* const removed = queue.removeWhere(track => track.source === "youtube");
|
|
193
|
-
*/
|
|
194
|
-
removeWhere(predicate: (track: Track, index: number) => boolean): Track[] {
|
|
195
|
-
const removed: Track[] = [];
|
|
196
|
-
for (let i = this.tracks.length - 1; i >= 0; i--) {
|
|
197
|
-
if (predicate(this.tracks[i], i)) {
|
|
198
|
-
removed.unshift(this.tracks.splice(i, 1)[0]);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return removed;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Get the next track in the queue
|
|
206
|
-
*
|
|
207
|
-
* @param {boolean} ignoreLoop - Ignore the loop mode
|
|
208
|
-
* @returns {Track | null} The next track or null
|
|
209
|
-
* @example
|
|
210
|
-
* const nextTrack = queue.next();
|
|
211
|
-
* console.log(`Next track: ${nextTrack?.title}`);
|
|
212
|
-
*/
|
|
213
|
-
next(ignoreLoop = false): Track | null {
|
|
214
|
-
// Handle track loop
|
|
215
|
-
if (this.current && this._loop === "track" && !ignoreLoop) {
|
|
216
|
-
return this.current;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Save current to history before moving to next
|
|
220
|
-
if (this.current) {
|
|
221
|
-
this.addToHistory(this.current);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Get next track
|
|
225
|
-
this.current = this.tracks.shift() || null;
|
|
226
|
-
|
|
227
|
-
// Handle queue loop
|
|
228
|
-
if (!this.current && this._loop === "queue" && this.history.length > 0 && !ignoreLoop) {
|
|
229
|
-
this.tracks = [...this.history];
|
|
230
|
-
this.history = [];
|
|
231
|
-
this.current = this.tracks.shift() || null;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return this.current;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Add track to history with size limit
|
|
239
|
-
*/
|
|
240
|
-
private addToHistory(track: Track): void {
|
|
241
|
-
this.history.push(track);
|
|
242
|
-
if (this.history.length > this.MAX_HISTORY_SIZE) {
|
|
243
|
-
this.history.shift();
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Clear all tracks from the queue
|
|
249
|
-
*
|
|
250
|
-
* @example
|
|
251
|
-
* queue.clear();
|
|
252
|
-
*/
|
|
253
|
-
clear(): void {
|
|
254
|
-
this.tracks = [];
|
|
255
|
-
// Optionally reset current track? Usually not, but provide option
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Clear history
|
|
260
|
-
*
|
|
261
|
-
* @example
|
|
262
|
-
* queue.clearHistory();
|
|
263
|
-
*/
|
|
264
|
-
clearHistory(): void {
|
|
265
|
-
this.history = [];
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Reset entire queue (current, history, tracks)
|
|
270
|
-
*
|
|
271
|
-
* @example
|
|
272
|
-
* queue.reset();
|
|
273
|
-
*/
|
|
274
|
-
reset(): void {
|
|
275
|
-
this.tracks = [];
|
|
276
|
-
this.current = null;
|
|
277
|
-
this.history = [];
|
|
278
|
-
this.related = [];
|
|
279
|
-
this.willnext = null;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Enable or disable auto-play
|
|
284
|
-
*
|
|
285
|
-
* @param {boolean} value - Enable/disable auto-play
|
|
286
|
-
* @returns {boolean} Current auto-play state
|
|
287
|
-
* @example
|
|
288
|
-
* queue.autoPlay(true);
|
|
289
|
-
* queue.autoPlay(); // Get current auto-play state
|
|
290
|
-
*/
|
|
291
|
-
autoPlay(value?: boolean): boolean {
|
|
292
|
-
if (typeof value !== "undefined") {
|
|
293
|
-
this._autoPlay = value;
|
|
294
|
-
}
|
|
295
|
-
return this._autoPlay;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Set the loop mode
|
|
300
|
-
*
|
|
301
|
-
* @param {LoopMode} mode - Loop mode to set
|
|
302
|
-
* @returns {LoopMode} The loop mode
|
|
303
|
-
* @example
|
|
304
|
-
* queue.loop("track");
|
|
305
|
-
*/
|
|
306
|
-
loop(mode?: LoopMode): LoopMode {
|
|
307
|
-
if (mode) {
|
|
308
|
-
this._loop = mode;
|
|
309
|
-
}
|
|
310
|
-
return this._loop;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Check if queue is currently looping
|
|
315
|
-
*
|
|
316
|
-
* @returns {boolean} True if looping
|
|
317
|
-
*/
|
|
318
|
-
isLooping(): boolean {
|
|
319
|
-
return this._loop !== "off";
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Get current loop mode
|
|
324
|
-
*
|
|
325
|
-
* @returns {LoopMode} Current loop mode
|
|
326
|
-
*/
|
|
327
|
-
getLoopMode(): LoopMode {
|
|
328
|
-
return this._loop;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Shuffle the queue
|
|
333
|
-
*
|
|
334
|
-
* @example
|
|
335
|
-
* queue.shuffle();
|
|
336
|
-
*/
|
|
337
|
-
shuffle(): void {
|
|
338
|
-
// Fisher-Yates shuffle
|
|
339
|
-
for (let i = this.tracks.length - 1; i > 0; i--) {
|
|
340
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
341
|
-
[this.tracks[i], this.tracks[j]] = [this.tracks[j], this.tracks[i]];
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Move a track from one position to another
|
|
347
|
-
*
|
|
348
|
-
* @param {number} fromIndex - Current index
|
|
349
|
-
* @param {number} toIndex - Target index
|
|
350
|
-
* @returns {boolean} True if move was successful
|
|
351
|
-
* @example
|
|
352
|
-
* queue.move(3, 0); // Move track at index 3 to position 0
|
|
353
|
-
*/
|
|
354
|
-
move(fromIndex: number, toIndex: number): boolean {
|
|
355
|
-
if (fromIndex < 0 || fromIndex >= this.tracks.length) return false;
|
|
356
|
-
if (toIndex < 0 || toIndex >= this.tracks.length) return false;
|
|
357
|
-
if (fromIndex === toIndex) return true;
|
|
358
|
-
|
|
359
|
-
const [track] = this.tracks.splice(fromIndex, 1);
|
|
360
|
-
this.tracks.splice(toIndex, 0, track);
|
|
361
|
-
return true;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Swap two tracks in the queue
|
|
366
|
-
*
|
|
367
|
-
* @param {number} indexA - First track index
|
|
368
|
-
* @param {number} indexB - Second track index
|
|
369
|
-
* @returns {boolean} True if swap was successful
|
|
370
|
-
* @example
|
|
371
|
-
* queue.swap(0, 3);
|
|
372
|
-
*/
|
|
373
|
-
swap(indexA: number, indexB: number): boolean {
|
|
374
|
-
if (indexA < 0 || indexA >= this.tracks.length) return false;
|
|
375
|
-
if (indexB < 0 || indexB >= this.tracks.length) return false;
|
|
376
|
-
if (indexA === indexB) return true;
|
|
377
|
-
|
|
378
|
-
[this.tracks[indexA], this.tracks[indexB]] = [this.tracks[indexB], this.tracks[indexA]];
|
|
379
|
-
return true;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Get the size of the queue
|
|
384
|
-
*/
|
|
385
|
-
get size(): number {
|
|
386
|
-
return this.tracks.length;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Check if the queue is empty
|
|
391
|
-
*/
|
|
392
|
-
get isEmpty(): boolean {
|
|
393
|
-
return this.tracks.length === 0;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Get the current track
|
|
398
|
-
*/
|
|
399
|
-
get currentTrack(): Track | null {
|
|
400
|
-
return this.current;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
*
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
*
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
*
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
*
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
*
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
*
|
|
528
|
-
*/
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
*
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
this.
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
*
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
1
|
+
import { Track, LoopMode } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manages the track queue for a player.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* // Basic queue operations
|
|
8
|
+
* const queue = player.queue;
|
|
9
|
+
*
|
|
10
|
+
* // Add single track
|
|
11
|
+
* queue.add(track);
|
|
12
|
+
*
|
|
13
|
+
* // Add multiple tracks
|
|
14
|
+
* queue.add([track1, track2, track3]);
|
|
15
|
+
*
|
|
16
|
+
* // Queue controls
|
|
17
|
+
* queue.shuffle(); // Randomize order
|
|
18
|
+
* queue.clear(); // Remove all tracks
|
|
19
|
+
* queue.autoPlay(true); // Enable auto-play
|
|
20
|
+
*
|
|
21
|
+
* // Get queue information
|
|
22
|
+
* console.log(`Queue length: ${queue.length}`);
|
|
23
|
+
* console.log(`Current track: ${queue.current?.title}`);
|
|
24
|
+
* console.log(`Is empty: ${queue.isEmpty}`);
|
|
25
|
+
* console.log(`Is playing: ${queue.isPlaying}`);
|
|
26
|
+
*
|
|
27
|
+
* // Loop modes
|
|
28
|
+
* queue.setLoopMode("track"); // Loop current track
|
|
29
|
+
* queue.setLoopMode("queue"); // Loop entire queue
|
|
30
|
+
* queue.setLoopMode("off"); // No loop
|
|
31
|
+
*
|
|
32
|
+
* // Remove specific track
|
|
33
|
+
* const removed = queue.remove(0); // Remove first track
|
|
34
|
+
* if (removed) {
|
|
35
|
+
* console.log(`Removed: ${removed.title}`);
|
|
36
|
+
* }
|
|
37
|
+
*/
|
|
38
|
+
export class Queue {
|
|
39
|
+
private tracks: Track[] = [];
|
|
40
|
+
private current: Track | null = null;
|
|
41
|
+
private history: Track[] = [];
|
|
42
|
+
private related: Track[] = [];
|
|
43
|
+
private _autoPlay = false;
|
|
44
|
+
private _loop: LoopMode = "off";
|
|
45
|
+
private willnext: Track | null = null;
|
|
46
|
+
|
|
47
|
+
// Configuration
|
|
48
|
+
private readonly MAX_HISTORY_SIZE = 200;
|
|
49
|
+
private readonly MAX_QUEUE_SIZE = 1000; // Prevent memory issues
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Add track(s) to the queue
|
|
53
|
+
*
|
|
54
|
+
* @param {Track | Track[]} track - Track or array of tracks to add
|
|
55
|
+
* @returns {number} New queue size
|
|
56
|
+
* @example
|
|
57
|
+
* queue.add(track);
|
|
58
|
+
* queue.add([track1, track2, track3]);
|
|
59
|
+
*/
|
|
60
|
+
add(track: Track): number {
|
|
61
|
+
if (this.tracks.length >= this.MAX_QUEUE_SIZE) {
|
|
62
|
+
throw new Error(`Queue size limit reached (${this.MAX_QUEUE_SIZE})`);
|
|
63
|
+
}
|
|
64
|
+
this.tracks.push(track);
|
|
65
|
+
return this.tracks.length;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Add multiple tracks to the queue
|
|
70
|
+
*
|
|
71
|
+
* @param {Track[]} tracks - Tracks to add
|
|
72
|
+
* @returns {number} New queue size
|
|
73
|
+
* @example
|
|
74
|
+
* queue.addMultiple([track1, track2, track3]);
|
|
75
|
+
*/
|
|
76
|
+
addMultiple(tracks: Track[]): number {
|
|
77
|
+
if (this.tracks.length + tracks.length > this.MAX_QUEUE_SIZE) {
|
|
78
|
+
throw new Error(`Adding ${tracks.length} tracks would exceed queue size limit (${this.MAX_QUEUE_SIZE})`);
|
|
79
|
+
}
|
|
80
|
+
this.tracks.push(...tracks);
|
|
81
|
+
return this.tracks.length;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Insert a track at a specific upcoming position (0 = next)
|
|
86
|
+
*
|
|
87
|
+
* @param {Track} track - Track to insert
|
|
88
|
+
* @param {number} index - Index to insert the track at
|
|
89
|
+
* @returns {number} New queue size
|
|
90
|
+
* @example
|
|
91
|
+
* queue.insert(track, 0);
|
|
92
|
+
*/
|
|
93
|
+
insert(track: Track, index: number): number {
|
|
94
|
+
if (this.tracks.length >= this.MAX_QUEUE_SIZE) {
|
|
95
|
+
throw new Error(`Queue size limit reached (${this.MAX_QUEUE_SIZE})`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!Number.isFinite(index)) {
|
|
99
|
+
this.tracks.push(track);
|
|
100
|
+
return this.tracks.length;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const i = Math.max(0, Math.min(Math.floor(index), this.tracks.length));
|
|
104
|
+
|
|
105
|
+
if (i === this.tracks.length) {
|
|
106
|
+
this.tracks.push(track);
|
|
107
|
+
} else if (i <= 0) {
|
|
108
|
+
this.tracks.unshift(track);
|
|
109
|
+
} else {
|
|
110
|
+
this.tracks.splice(i, 0, track);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return this.tracks.length;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Insert multiple tracks at a specific upcoming position, preserving order
|
|
118
|
+
*
|
|
119
|
+
* @param {Track[]} tracks - Tracks to insert
|
|
120
|
+
* @param {number} index - Index to insert the tracks at
|
|
121
|
+
* @returns {number} New queue size
|
|
122
|
+
* @example
|
|
123
|
+
* queue.insertMultiple([track1, track2, track3], 0);
|
|
124
|
+
*/
|
|
125
|
+
insertMultiple(tracks: Track[], index: number): number {
|
|
126
|
+
if (!Array.isArray(tracks) || tracks.length === 0) return this.tracks.length;
|
|
127
|
+
|
|
128
|
+
if (this.tracks.length + tracks.length > this.MAX_QUEUE_SIZE) {
|
|
129
|
+
throw new Error(`Inserting ${tracks.length} tracks would exceed queue size limit (${this.MAX_QUEUE_SIZE})`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!Number.isFinite(index)) {
|
|
133
|
+
this.tracks.push(...tracks);
|
|
134
|
+
return this.tracks.length;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const i = Math.max(0, Math.min(Math.floor(index), this.tracks.length));
|
|
138
|
+
|
|
139
|
+
if (i === 0) {
|
|
140
|
+
this.tracks = [...tracks, ...this.tracks];
|
|
141
|
+
} else if (i === this.tracks.length) {
|
|
142
|
+
this.tracks.push(...tracks);
|
|
143
|
+
} else {
|
|
144
|
+
this.tracks.splice(i, 0, ...tracks);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return this.tracks.length;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Remove a track from the queue
|
|
152
|
+
*
|
|
153
|
+
* @param {number} index - Index of track to remove
|
|
154
|
+
* @returns {Track | null} Removed track or null
|
|
155
|
+
* @example
|
|
156
|
+
* const removed = queue.remove(0);
|
|
157
|
+
* console.log(`Removed: ${removed?.title}`);
|
|
158
|
+
*/
|
|
159
|
+
remove(index: number): Track | null {
|
|
160
|
+
if (index < 0 || index >= this.tracks.length) return null;
|
|
161
|
+
const removed = this.tracks.splice(index, 1)[0];
|
|
162
|
+
return removed;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Remove multiple tracks by indices
|
|
167
|
+
*
|
|
168
|
+
* @param {number[]} indices - Array of indices to remove
|
|
169
|
+
* @returns {Track[]} Removed tracks
|
|
170
|
+
* @example
|
|
171
|
+
* const removed = queue.removeMultiple([0, 2, 5]);
|
|
172
|
+
*/
|
|
173
|
+
removeMultiple(indices: number[]): Track[] {
|
|
174
|
+
const sorted = [...new Set(indices)].sort((a, b) => b - a);
|
|
175
|
+
const removed: Track[] = [];
|
|
176
|
+
|
|
177
|
+
for (const index of sorted) {
|
|
178
|
+
if (index >= 0 && index < this.tracks.length) {
|
|
179
|
+
removed.unshift(this.tracks.splice(index, 1)[0]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return removed;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Remove tracks by predicate
|
|
188
|
+
*
|
|
189
|
+
* @param {(track: Track, index: number) => boolean} predicate - Filter function
|
|
190
|
+
* @returns {Track[]} Removed tracks
|
|
191
|
+
* @example
|
|
192
|
+
* const removed = queue.removeWhere(track => track.source === "youtube");
|
|
193
|
+
*/
|
|
194
|
+
removeWhere(predicate: (track: Track, index: number) => boolean): Track[] {
|
|
195
|
+
const removed: Track[] = [];
|
|
196
|
+
for (let i = this.tracks.length - 1; i >= 0; i--) {
|
|
197
|
+
if (predicate(this.tracks[i], i)) {
|
|
198
|
+
removed.unshift(this.tracks.splice(i, 1)[0]);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return removed;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get the next track in the queue
|
|
206
|
+
*
|
|
207
|
+
* @param {boolean} ignoreLoop - Ignore the loop mode
|
|
208
|
+
* @returns {Track | null} The next track or null
|
|
209
|
+
* @example
|
|
210
|
+
* const nextTrack = queue.next();
|
|
211
|
+
* console.log(`Next track: ${nextTrack?.title}`);
|
|
212
|
+
*/
|
|
213
|
+
next(ignoreLoop = false): Track | null {
|
|
214
|
+
// Handle track loop
|
|
215
|
+
if (this.current && this._loop === "track" && !ignoreLoop) {
|
|
216
|
+
return this.current;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Save current to history before moving to next
|
|
220
|
+
if (this.current) {
|
|
221
|
+
this.addToHistory(this.current);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Get next track
|
|
225
|
+
this.current = this.tracks.shift() || null;
|
|
226
|
+
|
|
227
|
+
// Handle queue loop
|
|
228
|
+
if (!this.current && this._loop === "queue" && this.history.length > 0 && !ignoreLoop) {
|
|
229
|
+
this.tracks = [...this.history];
|
|
230
|
+
this.history = [];
|
|
231
|
+
this.current = this.tracks.shift() || null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return this.current;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Add track to history with size limit
|
|
239
|
+
*/
|
|
240
|
+
private addToHistory(track: Track): void {
|
|
241
|
+
this.history.push(track);
|
|
242
|
+
if (this.history.length > this.MAX_HISTORY_SIZE) {
|
|
243
|
+
this.history.shift();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Clear all tracks from the queue
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* queue.clear();
|
|
252
|
+
*/
|
|
253
|
+
clear(): void {
|
|
254
|
+
this.tracks = [];
|
|
255
|
+
// Optionally reset current track? Usually not, but provide option
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Clear history
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* queue.clearHistory();
|
|
263
|
+
*/
|
|
264
|
+
clearHistory(): void {
|
|
265
|
+
this.history = [];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Reset entire queue (current, history, tracks)
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* queue.reset();
|
|
273
|
+
*/
|
|
274
|
+
reset(): void {
|
|
275
|
+
this.tracks = [];
|
|
276
|
+
this.current = null;
|
|
277
|
+
this.history = [];
|
|
278
|
+
this.related = [];
|
|
279
|
+
this.willnext = null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Enable or disable auto-play
|
|
284
|
+
*
|
|
285
|
+
* @param {boolean} value - Enable/disable auto-play
|
|
286
|
+
* @returns {boolean} Current auto-play state
|
|
287
|
+
* @example
|
|
288
|
+
* queue.autoPlay(true);
|
|
289
|
+
* queue.autoPlay(); // Get current auto-play state
|
|
290
|
+
*/
|
|
291
|
+
autoPlay(value?: boolean): boolean {
|
|
292
|
+
if (typeof value !== "undefined") {
|
|
293
|
+
this._autoPlay = value;
|
|
294
|
+
}
|
|
295
|
+
return this._autoPlay;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Set the loop mode
|
|
300
|
+
*
|
|
301
|
+
* @param {LoopMode} mode - Loop mode to set
|
|
302
|
+
* @returns {LoopMode} The loop mode
|
|
303
|
+
* @example
|
|
304
|
+
* queue.loop("track");
|
|
305
|
+
*/
|
|
306
|
+
loop(mode?: LoopMode): LoopMode {
|
|
307
|
+
if (mode) {
|
|
308
|
+
this._loop = mode;
|
|
309
|
+
}
|
|
310
|
+
return this._loop;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Check if queue is currently looping
|
|
315
|
+
*
|
|
316
|
+
* @returns {boolean} True if looping
|
|
317
|
+
*/
|
|
318
|
+
isLooping(): boolean {
|
|
319
|
+
return this._loop !== "off";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Get current loop mode
|
|
324
|
+
*
|
|
325
|
+
* @returns {LoopMode} Current loop mode
|
|
326
|
+
*/
|
|
327
|
+
getLoopMode(): LoopMode {
|
|
328
|
+
return this._loop;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Shuffle the queue
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* queue.shuffle();
|
|
336
|
+
*/
|
|
337
|
+
shuffle(): void {
|
|
338
|
+
// Fisher-Yates shuffle
|
|
339
|
+
for (let i = this.tracks.length - 1; i > 0; i--) {
|
|
340
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
341
|
+
[this.tracks[i], this.tracks[j]] = [this.tracks[j], this.tracks[i]];
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Move a track from one position to another
|
|
347
|
+
*
|
|
348
|
+
* @param {number} fromIndex - Current index
|
|
349
|
+
* @param {number} toIndex - Target index
|
|
350
|
+
* @returns {boolean} True if move was successful
|
|
351
|
+
* @example
|
|
352
|
+
* queue.move(3, 0); // Move track at index 3 to position 0
|
|
353
|
+
*/
|
|
354
|
+
move(fromIndex: number, toIndex: number): boolean {
|
|
355
|
+
if (fromIndex < 0 || fromIndex >= this.tracks.length) return false;
|
|
356
|
+
if (toIndex < 0 || toIndex >= this.tracks.length) return false;
|
|
357
|
+
if (fromIndex === toIndex) return true;
|
|
358
|
+
|
|
359
|
+
const [track] = this.tracks.splice(fromIndex, 1);
|
|
360
|
+
this.tracks.splice(toIndex, 0, track);
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Swap two tracks in the queue
|
|
366
|
+
*
|
|
367
|
+
* @param {number} indexA - First track index
|
|
368
|
+
* @param {number} indexB - Second track index
|
|
369
|
+
* @returns {boolean} True if swap was successful
|
|
370
|
+
* @example
|
|
371
|
+
* queue.swap(0, 3);
|
|
372
|
+
*/
|
|
373
|
+
swap(indexA: number, indexB: number): boolean {
|
|
374
|
+
if (indexA < 0 || indexA >= this.tracks.length) return false;
|
|
375
|
+
if (indexB < 0 || indexB >= this.tracks.length) return false;
|
|
376
|
+
if (indexA === indexB) return true;
|
|
377
|
+
|
|
378
|
+
[this.tracks[indexA], this.tracks[indexB]] = [this.tracks[indexB], this.tracks[indexA]];
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Get the size of the queue
|
|
384
|
+
*/
|
|
385
|
+
get size(): number {
|
|
386
|
+
return this.tracks.length;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Check if the queue is empty
|
|
391
|
+
*/
|
|
392
|
+
get isEmpty(): boolean {
|
|
393
|
+
return this.tracks.length === 0;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Get the current track
|
|
398
|
+
*/
|
|
399
|
+
get currentTrack(): Track | null {
|
|
400
|
+
return this.current;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Force set current playing track.
|
|
405
|
+
*
|
|
406
|
+
* Mainly used internally for playback synchronization,
|
|
407
|
+
* playback mirroring, restoring player state,
|
|
408
|
+
* or forward-mode shared audio sessions.
|
|
409
|
+
*
|
|
410
|
+
* This does NOT modify queue order/history automatically.
|
|
411
|
+
* It only updates the current active track reference.
|
|
412
|
+
*
|
|
413
|
+
* @param {Track | null} track - Track to set as current
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* queue.setCurrentTrack(track);
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* queue.setCurrentTrack(null);
|
|
420
|
+
*/
|
|
421
|
+
setCurrentTrack(track: Track | null): void {
|
|
422
|
+
this.current = track;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Get the previous tracks
|
|
427
|
+
*/
|
|
428
|
+
get previousTracks(): Track[] {
|
|
429
|
+
return [...this.history];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Get the number of previous tracks
|
|
434
|
+
*/
|
|
435
|
+
get previousTracksCount(): number {
|
|
436
|
+
return this.history.length;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Get the next track
|
|
441
|
+
*/
|
|
442
|
+
get nextTrack(): Track | null {
|
|
443
|
+
return this.tracks[0] || null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Get the last track in the queue
|
|
448
|
+
*/
|
|
449
|
+
get lastTrack(): Track | null {
|
|
450
|
+
return this.tracks[this.tracks.length - 1] || null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Move back to the previously played track.
|
|
455
|
+
* Makes the current track the next upcoming track, then sets previous as current.
|
|
456
|
+
*
|
|
457
|
+
* @returns {Track | null} The previous track or null
|
|
458
|
+
* @example
|
|
459
|
+
* const previousTrack = queue.previous();
|
|
460
|
+
* console.log(`Previous track: ${previousTrack?.title}`);
|
|
461
|
+
*/
|
|
462
|
+
previous(): Track | null {
|
|
463
|
+
if (this.history.length === 0) return null;
|
|
464
|
+
|
|
465
|
+
if (this.current) {
|
|
466
|
+
this.tracks.unshift(this.current);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
this.current = this.history.pop() || null;
|
|
470
|
+
return this.current;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Jump to a specific track in history
|
|
475
|
+
*
|
|
476
|
+
* @param {number} stepsBack - Number of steps back in history (1 = previous)
|
|
477
|
+
* @returns {Track | null} The jumped-to track or null
|
|
478
|
+
* @example
|
|
479
|
+
* queue.jumpToHistory(2); // Go back 2 tracks
|
|
480
|
+
*/
|
|
481
|
+
jumpToHistory(stepsBack: number): Track | null {
|
|
482
|
+
if (stepsBack <= 0 || stepsBack > this.history.length) return null;
|
|
483
|
+
|
|
484
|
+
const targetIndex = this.history.length - stepsBack;
|
|
485
|
+
if (targetIndex < 0) return null;
|
|
486
|
+
|
|
487
|
+
// Save current track to queue if exists
|
|
488
|
+
if (this.current) {
|
|
489
|
+
this.tracks.unshift(this.current);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Get tracks after target to push back to queue
|
|
493
|
+
const tracksAfterTarget = this.history.splice(targetIndex + 1);
|
|
494
|
+
this.current = this.history.pop() || null;
|
|
495
|
+
|
|
496
|
+
// Push tracks after target back to queue (in reverse order to maintain sequence)
|
|
497
|
+
for (let i = tracksAfterTarget.length - 1; i >= 0; i--) {
|
|
498
|
+
this.tracks.unshift(tracksAfterTarget[i]);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return this.current;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Get the next track (for auto-play)
|
|
506
|
+
*
|
|
507
|
+
* @param {Track} track - The next track
|
|
508
|
+
* @returns {Track | null} The next track or null
|
|
509
|
+
* @example
|
|
510
|
+
* const nextTrack = queue.willNextTrack();
|
|
511
|
+
* console.log(`Next track: ${nextTrack?.title}`);
|
|
512
|
+
*/
|
|
513
|
+
willNextTrack(track?: Track): Track | null {
|
|
514
|
+
if (track) {
|
|
515
|
+
this.willnext = track;
|
|
516
|
+
}
|
|
517
|
+
return this.willnext;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Get the related tracks
|
|
522
|
+
*
|
|
523
|
+
* @param {Track[]} track - The related tracks
|
|
524
|
+
* @returns {Track[] | null} The related tracks or null
|
|
525
|
+
* @example
|
|
526
|
+
* const relatedTracks = queue.relatedTracks();
|
|
527
|
+
* console.log(`Related tracks: ${relatedTracks?.length}`);
|
|
528
|
+
*/
|
|
529
|
+
relatedTracks(track?: Track[]): Track[] | null {
|
|
530
|
+
if (track) {
|
|
531
|
+
this.related = track;
|
|
532
|
+
}
|
|
533
|
+
return this.related;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Get all tracks in the queue
|
|
538
|
+
*
|
|
539
|
+
* @returns {Track[]} Copy of tracks array
|
|
540
|
+
* @example
|
|
541
|
+
* const tracks = queue.getTracks();
|
|
542
|
+
* console.log(`Tracks: ${tracks.length}`);
|
|
543
|
+
*/
|
|
544
|
+
getTracks(): Track[] {
|
|
545
|
+
return [...this.tracks];
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Get serializable queue data
|
|
550
|
+
*/
|
|
551
|
+
toJSON(): object {
|
|
552
|
+
return {
|
|
553
|
+
tracks: this.tracks,
|
|
554
|
+
current: this.current,
|
|
555
|
+
history: this.history,
|
|
556
|
+
size: this.size,
|
|
557
|
+
loopMode: this._loop,
|
|
558
|
+
autoPlay: this._autoPlay,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Restore queue from serialized data
|
|
564
|
+
*/
|
|
565
|
+
fromJSON(data: { tracks: Track[]; current: Track | null; history: Track[]; loopMode: LoopMode; autoPlay: boolean }): void {
|
|
566
|
+
this.tracks = [...data.tracks];
|
|
567
|
+
this.current = data.current;
|
|
568
|
+
this.history = [...data.history];
|
|
569
|
+
this._loop = data.loopMode;
|
|
570
|
+
this._autoPlay = data.autoPlay;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Get a track at a specific index
|
|
574
|
+
*
|
|
575
|
+
* @param {number} index - The index of the track
|
|
576
|
+
* @returns {Track | null} The track or null
|
|
577
|
+
* @example
|
|
578
|
+
* const track = queue.getTrack(0);
|
|
579
|
+
* console.log(`Track: ${track?.title}`);
|
|
580
|
+
*/
|
|
581
|
+
getTrack(index: number): Track | null {
|
|
582
|
+
return this.tracks[index] || null;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Find tracks by predicate
|
|
587
|
+
*
|
|
588
|
+
* @param {(track: Track) => boolean} predicate - Search function
|
|
589
|
+
* @returns {Track[]} Matching tracks
|
|
590
|
+
* @example
|
|
591
|
+
* const youtubeTracks = queue.findTracks(track => track.source === "youtube");
|
|
592
|
+
*/
|
|
593
|
+
findTracks(predicate: (track: Track) => boolean): Track[] {
|
|
594
|
+
return this.tracks.filter(predicate);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Get the index of a track in the queue
|
|
599
|
+
*
|
|
600
|
+
* @param {string | Track} identifier - Track ID, URL, or Track object
|
|
601
|
+
* @returns {number} Index of the track, -1 if not found
|
|
602
|
+
* @example
|
|
603
|
+
* const index = queue.indexOf(track);
|
|
604
|
+
*/
|
|
605
|
+
indexOf(identifier: string | Track): number {
|
|
606
|
+
if (typeof identifier === "string") {
|
|
607
|
+
return this.tracks.findIndex((t) => t.id === identifier || t.url === identifier);
|
|
608
|
+
}
|
|
609
|
+
return this.tracks.findIndex((t) => t.id === identifier.id);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Check if a track exists in the queue
|
|
614
|
+
*
|
|
615
|
+
* @param {string | Track} identifier - Track ID, URL, or Track object
|
|
616
|
+
* @returns {boolean} True if track exists
|
|
617
|
+
*/
|
|
618
|
+
has(identifier: string | Track): boolean {
|
|
619
|
+
return this.indexOf(identifier) !== -1;
|
|
620
|
+
}
|
|
621
|
+
}
|