audioq 2.0.0

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.
@@ -0,0 +1,378 @@
1
+ /**
2
+ * @fileoverview Queue manipulation functions for the audioq package
3
+ * Provides advanced queue management including item removal, reordering, and clearing
4
+ */
5
+
6
+ import {
7
+ ExtendedAudioQueueChannel,
8
+ QueueManipulationResult,
9
+ QueueSnapshot,
10
+ QueueItem
11
+ } from './types';
12
+ import { audioChannels } from './info';
13
+ import { emitQueueChange } from './events';
14
+ import { cleanupProgressTracking } from './events';
15
+ import { createQueueSnapshot, getAudioInfoFromElement } from './utils';
16
+
17
+ /**
18
+ * Removes a specific item from the queue by its slot number (0-based index)
19
+ * Cannot remove the currently playing item (index 0) - use stopCurrentAudioInChannel instead
20
+ * @param queuedSlotNumber - Zero-based index of the item to remove (must be > 0)
21
+ * @param channelNumber - The channel number (defaults to 0)
22
+ * @returns Promise resolving to operation result with success status and updated queue
23
+ * @throws Error if trying to remove currently playing item or invalid slot number
24
+ * @example
25
+ * ```typescript
26
+ * // Remove the second item in queue (index 1)
27
+ * const result = await removeQueuedItem(1, 0);
28
+ * if (result.success) {
29
+ * console.log(`Removed item, queue now has ${result.updatedQueue.totalItems} items`);
30
+ * }
31
+ *
32
+ * // Remove the third item from channel 1
33
+ * await removeQueuedItem(2, 1);
34
+ * ```
35
+ */
36
+ export const removeQueuedItem = async (
37
+ queuedSlotNumber: number,
38
+ channelNumber: number = 0
39
+ ): Promise<QueueManipulationResult> => {
40
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
41
+
42
+ if (!channel) {
43
+ return {
44
+ error: `Channel ${channelNumber} does not exist`,
45
+ success: false
46
+ };
47
+ }
48
+
49
+ if (queuedSlotNumber < 0 || queuedSlotNumber >= channel.queue.length) {
50
+ return {
51
+ error:
52
+ `Invalid slot number ${queuedSlotNumber}. Queue has ${channel.queue.length} items ` +
53
+ `(indices 0-${channel.queue.length - 1})`,
54
+ success: false
55
+ };
56
+ }
57
+
58
+ if (queuedSlotNumber === 0) {
59
+ return {
60
+ error:
61
+ 'Cannot remove currently playing item (index 0). ' +
62
+ 'Use stopCurrentAudioInChannel() instead',
63
+ success: false
64
+ };
65
+ }
66
+
67
+ // Remove the audio element from the queue
68
+ const removedAudio: HTMLAudioElement = channel.queue.splice(queuedSlotNumber, 1)[0];
69
+
70
+ // Clean up any progress tracking for the removed audio
71
+ cleanupProgressTracking(removedAudio, channelNumber, audioChannels);
72
+
73
+ // Emit queue change event
74
+ emitQueueChange(channelNumber, audioChannels);
75
+
76
+ const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
77
+
78
+ return {
79
+ success: true,
80
+ updatedQueue: updatedQueue ?? undefined
81
+ };
82
+ };
83
+
84
+ /**
85
+ * Reorders a queue item by moving it from one position to another
86
+ * Cannot reorder the currently playing item (index 0)
87
+ * @param currentQueuedSlotNumber - Current zero-based index of the item to move (must be > 0)
88
+ * @param newQueuedSlotNumber - New zero-based index where the item should be placed (must be > 0)
89
+ * @param channelNumber - The channel number (defaults to 0)
90
+ * @returns Promise resolving to operation result with success status and updated queue
91
+ * @throws Error if trying to reorder currently playing item or invalid slot numbers
92
+ * @example
93
+ * ```typescript
94
+ * // Move item from position 2 to position 1 (make it play next)
95
+ * const result = await reorderQueue(2, 1, 0);
96
+ * if (result.success) {
97
+ * console.log('Item moved successfully');
98
+ * }
99
+ *
100
+ * // Move item from position 1 to end of queue
101
+ * await reorderQueue(1, 4, 0); // Assuming queue has 5+ items
102
+ * ```
103
+ */
104
+ export const reorderQueue = async (
105
+ currentQueuedSlotNumber: number,
106
+ newQueuedSlotNumber: number,
107
+ channelNumber: number = 0
108
+ ): Promise<QueueManipulationResult> => {
109
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
110
+
111
+ if (!channel) {
112
+ return {
113
+ error: `Channel ${channelNumber} does not exist`,
114
+ success: false
115
+ };
116
+ }
117
+
118
+ if (currentQueuedSlotNumber < 0 || currentQueuedSlotNumber >= channel.queue.length) {
119
+ return {
120
+ error:
121
+ `Invalid current slot number ${currentQueuedSlotNumber}. Queue has ` +
122
+ `${channel.queue.length} items (indices 0-${channel.queue.length - 1})`,
123
+ success: false
124
+ };
125
+ }
126
+
127
+ if (newQueuedSlotNumber < 0 || newQueuedSlotNumber >= channel.queue.length) {
128
+ return {
129
+ error:
130
+ `Invalid new slot number ${newQueuedSlotNumber}. Queue has ` +
131
+ `${channel.queue.length} items (indices 0-${channel.queue.length - 1})`,
132
+ success: false
133
+ };
134
+ }
135
+
136
+ if (currentQueuedSlotNumber === 0) {
137
+ return {
138
+ error:
139
+ 'Cannot reorder currently playing item (index 0). ' + 'Stop current audio first if needed',
140
+ success: false
141
+ };
142
+ }
143
+
144
+ if (newQueuedSlotNumber === 0) {
145
+ return {
146
+ error:
147
+ 'Cannot move item to currently playing position (index 0). ' +
148
+ 'Use queueAudioPriority() to add items to front of queue',
149
+ success: false
150
+ };
151
+ }
152
+
153
+ if (currentQueuedSlotNumber === newQueuedSlotNumber) {
154
+ // No change needed, but return success
155
+ const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
156
+ return {
157
+ success: true,
158
+ updatedQueue: updatedQueue ?? undefined
159
+ };
160
+ }
161
+
162
+ // Remove the item from its current position
163
+ const audioToMove: HTMLAudioElement = channel.queue.splice(currentQueuedSlotNumber, 1)[0];
164
+
165
+ // Insert it at the new position
166
+ channel.queue.splice(newQueuedSlotNumber, 0, audioToMove);
167
+
168
+ // Emit queue change event
169
+ emitQueueChange(channelNumber, audioChannels);
170
+
171
+ const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
172
+
173
+ return {
174
+ success: true,
175
+ updatedQueue: updatedQueue ?? undefined
176
+ };
177
+ };
178
+
179
+ /**
180
+ * Clears all queued audio items after the currently playing item
181
+ * The current audio will continue playing but nothing will follow it
182
+ * @param channelNumber - The channel number (defaults to 0)
183
+ * @returns Promise resolving to operation result with success status and updated queue
184
+ * @example
185
+ * ```typescript
186
+ * // Let current song finish but clear everything after it
187
+ * const result = await clearQueueAfterCurrent(0);
188
+ * if (result.success) {
189
+ * console.log(`Cleared queue, current audio will be the last to play`);
190
+ * }
191
+ * ```
192
+ */
193
+ export const clearQueueAfterCurrent = async (
194
+ channelNumber: number = 0
195
+ ): Promise<QueueManipulationResult> => {
196
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
197
+
198
+ if (!channel) {
199
+ // For empty/non-existent channels, we can consider this a successful no-op
200
+ // since there's nothing to clear anyway
201
+ return {
202
+ success: true,
203
+ updatedQueue: {
204
+ channelNumber,
205
+ currentIndex: -1,
206
+ isPaused: false,
207
+ items: [],
208
+ totalItems: 0,
209
+ volume: 1.0
210
+ }
211
+ };
212
+ }
213
+
214
+ if (channel.queue.length <= 1) {
215
+ // Nothing to clear - either empty queue or only current audio
216
+ const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
217
+ return {
218
+ success: true,
219
+ updatedQueue: updatedQueue ?? undefined
220
+ };
221
+ }
222
+
223
+ // Clean up progress tracking for all items except the current one
224
+ for (let i: number = 1; i < channel.queue.length; i++) {
225
+ cleanupProgressTracking(channel.queue[i], channelNumber, audioChannels);
226
+ }
227
+
228
+ // Keep only the currently playing audio (index 0)
229
+ channel.queue = channel.queue.slice(0, 1);
230
+
231
+ // Emit queue change event
232
+ emitQueueChange(channelNumber, audioChannels);
233
+
234
+ const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
235
+
236
+ return {
237
+ success: true,
238
+ updatedQueue: updatedQueue ?? undefined
239
+ };
240
+ };
241
+
242
+ /**
243
+ * Gets information about a specific queue item by its slot number
244
+ * @param queueSlotNumber - Zero-based index of the queue item
245
+ * @param channelNumber - The channel number (defaults to 0)
246
+ * @returns QueueItem information or null if slot doesn't exist
247
+ * @example
248
+ * ```typescript
249
+ * const itemInfo = getQueueItemInfo(1, 0);
250
+ * if (itemInfo) {
251
+ * console.log(`Next to play: ${itemInfo.fileName}`);
252
+ * console.log(`Duration: ${itemInfo.duration}ms`);
253
+ * }
254
+ * ```
255
+ */
256
+ export const getQueueItemInfo = (
257
+ queueSlotNumber: number,
258
+ channelNumber: number = 0
259
+ ): QueueItem | null => {
260
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
261
+
262
+ if (!channel || queueSlotNumber < 0 || queueSlotNumber >= channel.queue.length) {
263
+ return null;
264
+ }
265
+
266
+ const audio: HTMLAudioElement = channel.queue[queueSlotNumber];
267
+ const audioInfo = getAudioInfoFromElement(audio, channelNumber, audioChannels);
268
+
269
+ if (!audioInfo) {
270
+ return null;
271
+ }
272
+
273
+ const { duration, fileName, isLooping, isPlaying, src, volume } = audioInfo;
274
+
275
+ return {
276
+ duration,
277
+ fileName,
278
+ isCurrentlyPlaying: queueSlotNumber === 0 && isPlaying,
279
+ isLooping,
280
+ src,
281
+ volume
282
+ };
283
+ };
284
+
285
+ /**
286
+ * Gets the current queue length for a specific channel
287
+ * @param channelNumber - The channel number (defaults to 0)
288
+ * @returns Number of items in the queue, or 0 if channel doesn't exist
289
+ * @example
290
+ * ```typescript
291
+ * const queueSize = getQueueLength(0);
292
+ * console.log(`Channel 0 has ${queueSize} items in queue`);
293
+ * ```
294
+ */
295
+ export const getQueueLength = (channelNumber: number = 0): number => {
296
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
297
+ return channel ? channel.queue.length : 0;
298
+ };
299
+
300
+ /**
301
+ * Swaps the positions of two queue items
302
+ * Cannot swap with the currently playing item (index 0)
303
+ * @param slotA - Zero-based index of first item to swap (must be > 0)
304
+ * @param slotB - Zero-based index of second item to swap (must be > 0)
305
+ * @param channelNumber - The channel number (defaults to 0)
306
+ * @returns Promise resolving to operation result with success status and updated queue
307
+ * @example
308
+ * ```typescript
309
+ * // Swap the second and third items in queue
310
+ * const result = await swapQueueItems(1, 2, 0);
311
+ * if (result.success) {
312
+ * console.log('Items swapped successfully');
313
+ * }
314
+ * ```
315
+ */
316
+ export const swapQueueItems = async (
317
+ slotA: number,
318
+ slotB: number,
319
+ channelNumber: number = 0
320
+ ): Promise<QueueManipulationResult> => {
321
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
322
+
323
+ if (!channel) {
324
+ return {
325
+ error: `Channel ${channelNumber} does not exist`,
326
+ success: false
327
+ };
328
+ }
329
+
330
+ if (slotA < 0 || slotA >= channel.queue.length) {
331
+ return {
332
+ error:
333
+ `Invalid slot A ${slotA}. Queue has ${channel.queue.length} items ` +
334
+ `(indices 0-${channel.queue.length - 1})`,
335
+ success: false
336
+ };
337
+ }
338
+
339
+ if (slotB < 0 || slotB >= channel.queue.length) {
340
+ return {
341
+ error:
342
+ `Invalid slot B ${slotB}. Queue has ${channel.queue.length} items ` +
343
+ `(indices 0-${channel.queue.length - 1})`,
344
+ success: false
345
+ };
346
+ }
347
+
348
+ if (slotA === 0 || slotB === 0) {
349
+ return {
350
+ error: 'Cannot swap with currently playing item (index 0)',
351
+ success: false
352
+ };
353
+ }
354
+
355
+ if (slotA === slotB) {
356
+ // No change needed, but return success
357
+ const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
358
+ return {
359
+ success: true,
360
+ updatedQueue: updatedQueue ?? undefined
361
+ };
362
+ }
363
+
364
+ // Swap the audio elements
365
+ const temp: HTMLAudioElement = channel.queue[slotA];
366
+ channel.queue[slotA] = channel.queue[slotB];
367
+ channel.queue[slotB] = temp;
368
+
369
+ // Emit queue change event
370
+ emitQueueChange(channelNumber, audioChannels);
371
+
372
+ const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
373
+
374
+ return {
375
+ success: true,
376
+ updatedQueue: updatedQueue ?? undefined
377
+ };
378
+ };