ziplayer 0.3.3 → 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.
Files changed (35) hide show
  1. package/AGENTS.md +717 -653
  2. package/README.md +658 -639
  3. package/dist/extensions/BaseExtension.d.ts +10 -1
  4. package/dist/extensions/BaseExtension.d.ts.map +1 -1
  5. package/dist/extensions/BaseExtension.js +27 -1
  6. package/dist/extensions/BaseExtension.js.map +1 -1
  7. package/dist/extensions/index.d.ts.map +1 -1
  8. package/dist/extensions/index.js +24 -6
  9. package/dist/extensions/index.js.map +1 -1
  10. package/dist/plugins/index.d.ts.map +1 -1
  11. package/dist/plugins/index.js +104 -50
  12. package/dist/plugins/index.js.map +1 -1
  13. package/dist/structures/Player.d.ts +74 -43
  14. package/dist/structures/Player.d.ts.map +1 -1
  15. package/dist/structures/Player.js +440 -114
  16. package/dist/structures/Player.js.map +1 -1
  17. package/dist/structures/PlayerManager.d.ts +41 -6
  18. package/dist/structures/PlayerManager.d.ts.map +1 -1
  19. package/dist/structures/PlayerManager.js +94 -125
  20. package/dist/structures/PlayerManager.js.map +1 -1
  21. package/dist/types/extension.d.ts +3 -0
  22. package/dist/types/extension.d.ts.map +1 -1
  23. package/dist/types/index.d.ts +38 -11
  24. package/dist/types/index.d.ts.map +1 -1
  25. package/dist/types/index.js +7 -0
  26. package/dist/types/index.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/extensions/BaseExtension.ts +31 -1
  29. package/src/extensions/index.ts +30 -7
  30. package/src/plugins/index.ts +136 -53
  31. package/src/structures/Player.ts +2937 -2544
  32. package/src/structures/PlayerManager.ts +916 -955
  33. package/src/structures/Queue.ts +621 -621
  34. package/src/types/extension.ts +3 -0
  35. package/src/types/index.ts +43 -11
@@ -1,621 +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
- * 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
- }
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
+ }