tiny-essentials 1.28.7 → 1.28.9
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/changelog/1/28/9.md +17 -0
- package/dist/v1/TinyBasicsEs.min.js +1 -1
- package/dist/v1/TinyDebugger.min.js +1 -0
- package/dist/v1/TinyEssentials.min.js +1 -1
- package/dist/v1/TinyMediaPlayer.min.js +1 -1
- package/dist/v1/TinyRadioFm.min.js +1 -1
- package/dist/v1/TinyUploadClicker.min.js +1 -1
- package/dist/v1/basics/browserDetector.cjs +228 -0
- package/dist/v1/basics/browserDetector.d.mts +66 -0
- package/dist/v1/basics/browserDetector.mjs +207 -0
- package/dist/v1/basics/index.cjs +6 -0
- package/dist/v1/basics/index.d.mts +2 -1
- package/dist/v1/basics/index.mjs +2 -1
- package/dist/v1/build/TinyDebugger.cjs +7 -0
- package/dist/v1/build/TinyDebugger.d.mts +3 -0
- package/dist/v1/build/TinyDebugger.mjs +2 -0
- package/dist/v1/index.cjs +8 -0
- package/dist/v1/index.d.mts +3 -1
- package/dist/v1/index.mjs +4 -2
- package/dist/v1/libs/TinyDebugger.cjs +380 -0
- package/dist/v1/libs/TinyDebugger.d.mts +132 -0
- package/dist/v1/libs/TinyDebugger.mjs +351 -0
- package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.cjs +39 -3
- package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.d.mts +10 -0
- package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.mjs +35 -3
- package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.cjs +39 -3
- package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.d.mts +10 -0
- package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.mjs +35 -3
- package/dist/v1/libs/TinyMediaPlayer/Mock.cjs +333 -0
- package/dist/v1/libs/TinyMediaPlayer/Mock.d.mts +154 -0
- package/dist/v1/libs/TinyMediaPlayer/Mock.mjs +296 -0
- package/dist/v1/libs/TinyMediaPlayer/SoundCloud.cjs +30 -0
- package/dist/v1/libs/TinyMediaPlayer/SoundCloud.d.mts +10 -0
- package/dist/v1/libs/TinyMediaPlayer/SoundCloud.mjs +26 -0
- package/dist/v1/libs/TinyMediaPlayer/Youtube.cjs +39 -3
- package/dist/v1/libs/TinyMediaPlayer/Youtube.d.mts +10 -0
- package/dist/v1/libs/TinyMediaPlayer/Youtube.mjs +32 -3
- package/dist/v1/libs/TinyMediaPlayer/index.cjs +18 -0
- package/dist/v1/libs/TinyMediaPlayer/index.d.mts +10 -0
- package/dist/v1/libs/TinyMediaPlayer/index.mjs +16 -0
- package/dist/v1/libs/TinyMediaPlayer.cjs +156 -31
- package/dist/v1/libs/TinyMediaPlayer.d.mts +43 -7
- package/dist/v1/libs/TinyMediaPlayer.mjs +148 -31
- package/dist/v1/libs/TinyPromiseQueue.cjs +6 -4
- package/dist/v1/libs/TinyPromiseQueue.d.mts +8 -6
- package/dist/v1/libs/TinyPromiseQueue.mjs +6 -4
- package/dist/v1/libs/TinyRadioFm.cjs +271 -138
- package/dist/v1/libs/TinyRadioFm.d.mts +67 -3
- package/dist/v1/libs/TinyRadioFm.mjs +239 -123
- package/docs/v1/README.md +2 -0
- package/docs/v1/basics/browserDetector.md +135 -0
- package/docs/v1/libs/TinyDebugger.md +119 -0
- package/docs/v1/libs/TinyMediaPlayer.md +26 -22
- package/docs/v1/libs/TinyRadioFm.md +25 -3
- package/package.json +13 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import TinyDebugger from './TinyDebugger.mjs';
|
|
2
2
|
import { getMediaContentBase, getMediaContentMetadata, parseMediaMetadata, valMediaContentMetadata, } from '../basics/mediaContent.mjs';
|
|
3
3
|
import { createCheckDestroyed } from './utils.mjs';
|
|
4
4
|
import { BaseMediaAdapter } from './TinyMediaPlayer/index.mjs';
|
|
@@ -28,6 +28,9 @@ const checkDestroy = createCheckDestroyed('TinyMediaPlayer');
|
|
|
28
28
|
* @property {boolean} [repeatCurrentOnPrev=false] - If true, clicking 'previous' repeats the current track on the first click.
|
|
29
29
|
* @property {boolean} [smoothPlayPauseVolume=false] - If true, volume fades smoothly during play/pause transitions.
|
|
30
30
|
* @property {boolean} [smoothStopVolume=false] - If true, volume fades smoothly to zero when stopping.
|
|
31
|
+
* @property {boolean} [debugMode=false] - Whether to enable internal debug logging.
|
|
32
|
+
* @property {boolean} [useLogColors=false] - Whether to enable log color support.
|
|
33
|
+
* @property {Console} [logger=console] - A custom logger object (must implement console methods).
|
|
31
34
|
*/
|
|
32
35
|
/**
|
|
33
36
|
* Represents a search match containing the media object and its exact playlist index.
|
|
@@ -38,7 +41,7 @@ const checkDestroy = createCheckDestroyed('TinyMediaPlayer');
|
|
|
38
41
|
/**
|
|
39
42
|
* A universal media player manager capable of orchestrating multiple API adapters.
|
|
40
43
|
*/
|
|
41
|
-
class TinyMediaPlayer extends
|
|
44
|
+
class TinyMediaPlayer extends TinyDebugger {
|
|
42
45
|
static BaseMediaAdapter = BaseMediaAdapter;
|
|
43
46
|
/**
|
|
44
47
|
* @type {UnknownArtistGetter}
|
|
@@ -218,10 +221,16 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
218
221
|
* @throws {RangeError} If numeric options are out of valid ranges.
|
|
219
222
|
*/
|
|
220
223
|
constructor(options = {}) {
|
|
221
|
-
super();
|
|
222
224
|
if (!isValidObj(options)) {
|
|
223
225
|
throw new TypeError('Options must be a non-null object.');
|
|
224
226
|
}
|
|
227
|
+
// Debug Configuration
|
|
228
|
+
super({
|
|
229
|
+
id: '[_blue_TinyMediaPlayer_reset_] :debug:',
|
|
230
|
+
logger: options.logger ?? console,
|
|
231
|
+
debugMode: options.debugMode ?? false,
|
|
232
|
+
useLogColors: options.useLogColors ?? false,
|
|
233
|
+
});
|
|
225
234
|
// Volume configuration
|
|
226
235
|
if (options.persistVolume !== undefined) {
|
|
227
236
|
if (typeof options.persistVolume !== 'boolean') {
|
|
@@ -270,6 +279,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
270
279
|
}
|
|
271
280
|
if (this.#persistVolume)
|
|
272
281
|
this.#loadVolumeFromStorage();
|
|
282
|
+
this.log('info', 'Instance initialized.', { options });
|
|
273
283
|
}
|
|
274
284
|
// ==========================================
|
|
275
285
|
// STORAGE HELPERS
|
|
@@ -285,6 +295,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
285
295
|
const parsedVolume = parseFloat(storedValue);
|
|
286
296
|
if (!isNaN(parsedVolume) && parsedVolume >= 0 && parsedVolume <= 1) {
|
|
287
297
|
this.#volume = parsedVolume;
|
|
298
|
+
this.log('info', `Volume loaded from storage: ${this.#volume}`);
|
|
288
299
|
}
|
|
289
300
|
}
|
|
290
301
|
}
|
|
@@ -300,6 +311,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
300
311
|
try {
|
|
301
312
|
if (this.#persistVolume && typeof window !== 'undefined' && window.localStorage) {
|
|
302
313
|
window.localStorage.setItem(this.#volumeStorageKey, this.#volume.toString());
|
|
314
|
+
this.log('info', `Volume saved to storage: ${this.#volume}`);
|
|
303
315
|
}
|
|
304
316
|
}
|
|
305
317
|
catch (error) {
|
|
@@ -362,11 +374,14 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
362
374
|
const startVolume = adapter.getVolume();
|
|
363
375
|
const startTime = performance.now();
|
|
364
376
|
const duration = this.#fadeVolumeSpeed;
|
|
377
|
+
this.log('info', `Starting volume fade: ${startVolume} -> ${targetVolume}`);
|
|
365
378
|
return new Promise((resolve) => {
|
|
366
379
|
/** @param {number} currentTime */
|
|
367
380
|
const step = (currentTime) => {
|
|
368
|
-
if (signal.aborted)
|
|
381
|
+
if (signal.aborted) {
|
|
382
|
+
this.log('info', 'Volume fade aborted.');
|
|
369
383
|
return resolve();
|
|
384
|
+
}
|
|
370
385
|
const elapsed = currentTime - startTime;
|
|
371
386
|
const progress = Math.min(elapsed / duration, 1);
|
|
372
387
|
const calculatedVolume = startVolume + (targetVolume - startVolume) * progress;
|
|
@@ -377,6 +392,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
377
392
|
}
|
|
378
393
|
else {
|
|
379
394
|
adapter.setVolume(targetVolume);
|
|
395
|
+
this.log('info', `Volume fade completed: ${targetVolume}`);
|
|
380
396
|
resolve();
|
|
381
397
|
}
|
|
382
398
|
};
|
|
@@ -413,6 +429,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
413
429
|
}
|
|
414
430
|
}
|
|
415
431
|
this.#playlist = value;
|
|
432
|
+
this.log('info', `Playlist updated. New size: ${this.#playlist.length}`);
|
|
416
433
|
this.emit('playlistUpdate', this.playlist);
|
|
417
434
|
// Validation to correct currentIndex ensuring it aligns with the new playlist limits
|
|
418
435
|
if (this.#playlist.length === 0) {
|
|
@@ -435,6 +452,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
435
452
|
await this.play();
|
|
436
453
|
}
|
|
437
454
|
catch (error) {
|
|
455
|
+
this.log('error', 'Background transition error on playlist update:', error);
|
|
438
456
|
console.warn('[TinyMediaPlayer] Background transition error on playlist update:', error);
|
|
439
457
|
}
|
|
440
458
|
})();
|
|
@@ -460,6 +478,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
460
478
|
}
|
|
461
479
|
if (this.#currentIndex === value)
|
|
462
480
|
return; // Optimization: do nothing if index is the same
|
|
481
|
+
this.log('info', `Index change requested: ${this.#currentIndex} -> ${value}`);
|
|
463
482
|
const wasPlaying = this.#isPlaying;
|
|
464
483
|
let oldAdapter = null;
|
|
465
484
|
if (this.#currentIndex !== -1 && this.#playlist.length > 0) {
|
|
@@ -482,6 +501,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
482
501
|
await this.play();
|
|
483
502
|
}
|
|
484
503
|
catch (error) {
|
|
504
|
+
this.log('error', 'Background transition error on index update:', error);
|
|
485
505
|
console.warn('[TinyMediaPlayer] Background transition error on index update:', error);
|
|
486
506
|
}
|
|
487
507
|
})();
|
|
@@ -503,6 +523,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
503
523
|
throw new TypeError(`Loop mode must be one of: ${validModes.join(', ')}.`);
|
|
504
524
|
}
|
|
505
525
|
this.#loopMode = value;
|
|
526
|
+
this.log('info', `Loop mode changed to: ${this.#loopMode}`);
|
|
506
527
|
this.emit('loopModeChange', this.#loopMode);
|
|
507
528
|
}
|
|
508
529
|
/** @returns {boolean} The current random mode status. */
|
|
@@ -520,6 +541,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
520
541
|
throw new TypeError('Random mode state must be a boolean.');
|
|
521
542
|
}
|
|
522
543
|
this.#isRandom = value;
|
|
544
|
+
this.log('info', `Random mode: ${this.#isRandom}`);
|
|
523
545
|
this.emit('randomModeChange', this.#isRandom);
|
|
524
546
|
}
|
|
525
547
|
/** @returns {boolean} True if media is actively playing. */
|
|
@@ -546,6 +568,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
546
568
|
// Abort any ongoing fade to ensure manual control is immediate
|
|
547
569
|
if (this.#fadeController)
|
|
548
570
|
this.#fadeController.abort();
|
|
571
|
+
this.log('info', `Volume changed: ${this.#volume} -> ${value}`);
|
|
549
572
|
this.#volume = value;
|
|
550
573
|
this.#saveVolumeToStorage();
|
|
551
574
|
this.emit('volumeChange', this.#volume);
|
|
@@ -576,6 +599,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
576
599
|
throw new TypeError('Persist volume parameter must be a boolean.');
|
|
577
600
|
}
|
|
578
601
|
this.#persistVolume = value;
|
|
602
|
+
this.log('info', `Persist volume: ${this.#persistVolume}`);
|
|
579
603
|
if (value) {
|
|
580
604
|
this.#saveVolumeToStorage();
|
|
581
605
|
}
|
|
@@ -616,28 +640,32 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
616
640
|
return this.#isMuted;
|
|
617
641
|
}
|
|
618
642
|
/**
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
* @param {boolean} value - True to mute, false to unMute.
|
|
622
|
-
* @throws {TypeError} If the value is not a boolean.
|
|
643
|
+
* Mutes the current playback.
|
|
644
|
+
* @returns {Promise<void>}
|
|
623
645
|
*/
|
|
624
|
-
|
|
646
|
+
async mute() {
|
|
625
647
|
checkDestroy(this.#destroyed);
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
try {
|
|
632
|
-
const adapter = this.#getActiveAdapter();
|
|
633
|
-
if (adapter) {
|
|
634
|
-
value ? adapter.mute() : adapter.unMute();
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
catch (error) {
|
|
638
|
-
// If no active adapter, we just wait for the next play() call to apply the state
|
|
639
|
-
}
|
|
648
|
+
const adapter = this.#getActiveAdapter();
|
|
649
|
+
if (!adapter)
|
|
650
|
+
return;
|
|
651
|
+
await adapter.mute();
|
|
652
|
+
this.#isMuted = true;
|
|
640
653
|
this.emit('muteChange', this.#isMuted);
|
|
654
|
+
this.log('info', 'Playback muted successfully.');
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Unmutes the current playback.
|
|
658
|
+
* @returns {Promise<void>}
|
|
659
|
+
*/
|
|
660
|
+
async unMute() {
|
|
661
|
+
checkDestroy(this.#destroyed);
|
|
662
|
+
const adapter = this.#getActiveAdapter();
|
|
663
|
+
if (!adapter)
|
|
664
|
+
return;
|
|
665
|
+
await adapter.unMute();
|
|
666
|
+
this.#isMuted = false;
|
|
667
|
+
this.emit('muteChange', this.#isMuted);
|
|
668
|
+
this.log('info', 'Playback unmuted successfully.');
|
|
641
669
|
}
|
|
642
670
|
// ==========================================
|
|
643
671
|
// ADAPTER MANAGEMENT
|
|
@@ -686,6 +714,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
686
714
|
if (!(adapter instanceof BaseMediaAdapter)) {
|
|
687
715
|
throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
|
|
688
716
|
}
|
|
717
|
+
this.log('info', 'Registering new adapter:', adapter);
|
|
689
718
|
const events = new Map();
|
|
690
719
|
this.#adapterHandlers.set(adapter, events);
|
|
691
720
|
// Use the controlled helper to ensure each event is registered
|
|
@@ -705,6 +734,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
705
734
|
if (!(adapter instanceof BaseMediaAdapter)) {
|
|
706
735
|
throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
|
|
707
736
|
}
|
|
737
|
+
this.log('info', 'Removing adapter:', adapter);
|
|
708
738
|
this.#removeHandler(adapter);
|
|
709
739
|
return this.#adapters.delete(adapter);
|
|
710
740
|
}
|
|
@@ -718,6 +748,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
718
748
|
if (!(adapter instanceof BaseMediaAdapter)) {
|
|
719
749
|
throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
|
|
720
750
|
}
|
|
751
|
+
this.log('info', 'Destroying adapter:', adapter);
|
|
721
752
|
this.#removeHandler(adapter);
|
|
722
753
|
const result = this.#adapters.delete(adapter);
|
|
723
754
|
if (result)
|
|
@@ -741,6 +772,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
741
772
|
*/
|
|
742
773
|
destroyAllAdapters() {
|
|
743
774
|
checkDestroy(this.#destroyed);
|
|
775
|
+
this.log('info', 'Destroying all adapters.');
|
|
744
776
|
this.#adapters.forEach((adapter) => {
|
|
745
777
|
this.#removeHandler(adapter);
|
|
746
778
|
adapter.destroy();
|
|
@@ -752,6 +784,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
752
784
|
*/
|
|
753
785
|
clearAdapters() {
|
|
754
786
|
checkDestroy(this.#destroyed);
|
|
787
|
+
this.log('info', 'Clearing all adapters.');
|
|
755
788
|
this.#adapters.forEach((adapter) => this.#removeHandler(adapter));
|
|
756
789
|
this.#adapters.clear();
|
|
757
790
|
}
|
|
@@ -784,7 +817,9 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
784
817
|
const adapter = this.getMediaAdapter(currentContent);
|
|
785
818
|
if (adapter)
|
|
786
819
|
return adapter;
|
|
787
|
-
|
|
820
|
+
const errorMsg = `No compatible adapter found for content ID: ${currentContent.id}.`;
|
|
821
|
+
this.log('error', errorMsg);
|
|
822
|
+
throw new Error(errorMsg);
|
|
788
823
|
}
|
|
789
824
|
// ==========================================
|
|
790
825
|
// EVENT MANAGEMENT (CONTROLLED)
|
|
@@ -802,6 +837,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
802
837
|
// If the event is already in the list, do nothing to avoid duplicates
|
|
803
838
|
if (this.#adapterEventNames.has(eventName))
|
|
804
839
|
return;
|
|
840
|
+
this.log('info', `Adding adapter event listener: ${eventName}`);
|
|
805
841
|
this.#adapterEventNames.add(eventName);
|
|
806
842
|
// Synchronize: Add the listener to all already registered adapters
|
|
807
843
|
this.#adapters.forEach((adapter) => {
|
|
@@ -820,6 +856,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
820
856
|
}
|
|
821
857
|
if (!this.#adapterEventNames.has(eventName))
|
|
822
858
|
return;
|
|
859
|
+
this.log('info', `Removing adapter event listener: ${eventName}`);
|
|
823
860
|
this.#adapterEventNames.delete(eventName);
|
|
824
861
|
// Synchronize: Remove the listener from all registered adapters
|
|
825
862
|
this.#adapters.forEach((adapter) => {
|
|
@@ -861,6 +898,8 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
861
898
|
/** @type {(...args: any[]) => any} */
|
|
862
899
|
const handler = (...args) => {
|
|
863
900
|
if (!this.#destroyed) {
|
|
901
|
+
if (eventName !== 'timeupdate')
|
|
902
|
+
this.log('debug', `Adapter event triggered: ${eventName}`, ...args);
|
|
864
903
|
this.emit(eventName, ...args);
|
|
865
904
|
}
|
|
866
905
|
};
|
|
@@ -900,6 +939,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
900
939
|
const newContent = { ...getMediaContentBase(), ...getMediaContentMetadata(), ...content };
|
|
901
940
|
valMediaContentMetadata(newContent);
|
|
902
941
|
const newLength = this.#playlist.push(newContent);
|
|
942
|
+
this.log('info', `Track added. New length: ${newLength}`, newContent);
|
|
903
943
|
if (this.#currentIndex === -1) {
|
|
904
944
|
this.#currentIndex = 0;
|
|
905
945
|
this.emit('trackChange', this.#currentIndex);
|
|
@@ -952,6 +992,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
952
992
|
if (!this.existsTrack(index)) {
|
|
953
993
|
throw new RangeError(`Index ${index} is out of bounds for the current playlist.`);
|
|
954
994
|
}
|
|
995
|
+
this.log('info', `Removing track at index: ${index}`);
|
|
955
996
|
if (index === this.#currentIndex) {
|
|
956
997
|
// Stop the current track if it is the one being removed
|
|
957
998
|
await this.stop();
|
|
@@ -987,6 +1028,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
987
1028
|
if (typeof query !== 'string' && typeof query !== 'function') {
|
|
988
1029
|
throw new TypeError('Search query must be a string or a boolean evaluation function.');
|
|
989
1030
|
}
|
|
1031
|
+
this.log('info', 'Searching playlist...', { query });
|
|
990
1032
|
const results = [];
|
|
991
1033
|
for (let i = 0; i < this.#playlist.length; i++) {
|
|
992
1034
|
const track = this.#playlist[i];
|
|
@@ -1005,6 +1047,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1005
1047
|
results.push({ track, index: i });
|
|
1006
1048
|
}
|
|
1007
1049
|
}
|
|
1050
|
+
this.log('info', `Search completed. Found ${results.length} results.`);
|
|
1008
1051
|
return results;
|
|
1009
1052
|
}
|
|
1010
1053
|
/**
|
|
@@ -1012,6 +1055,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1012
1055
|
*/
|
|
1013
1056
|
async clearPlaylist() {
|
|
1014
1057
|
checkDestroy(this.#destroyed);
|
|
1058
|
+
this.log('info', 'Clearing entire playlist.');
|
|
1015
1059
|
await this.stop();
|
|
1016
1060
|
this.#playlist = [];
|
|
1017
1061
|
this.#currentIndex = -1;
|
|
@@ -1027,6 +1071,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1027
1071
|
*/
|
|
1028
1072
|
async play() {
|
|
1029
1073
|
checkDestroy(this.#destroyed);
|
|
1074
|
+
this.log('info', `Attempting to play track at index: ${this.#currentIndex}`);
|
|
1030
1075
|
const adapter = this.#getActiveAdapter();
|
|
1031
1076
|
if (!adapter)
|
|
1032
1077
|
return;
|
|
@@ -1037,11 +1082,18 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1037
1082
|
else {
|
|
1038
1083
|
adapter.setVolume(this.#volume);
|
|
1039
1084
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1085
|
+
try {
|
|
1086
|
+
await adapter.play(this.#playlist[this.#currentIndex]);
|
|
1087
|
+
if (this.#isMuted)
|
|
1088
|
+
await adapter.mute();
|
|
1089
|
+
this.#isPlaying = true;
|
|
1090
|
+
this.log('info', 'Playback started successfully.');
|
|
1091
|
+
this.emit('play', this.#currentIndex);
|
|
1092
|
+
}
|
|
1093
|
+
catch (error) {
|
|
1094
|
+
this.log('error', 'Failed to play track:', error);
|
|
1095
|
+
throw error;
|
|
1096
|
+
}
|
|
1045
1097
|
}
|
|
1046
1098
|
/**
|
|
1047
1099
|
* Pauses the current track.
|
|
@@ -1049,6 +1101,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1049
1101
|
*/
|
|
1050
1102
|
async pause() {
|
|
1051
1103
|
checkDestroy(this.#destroyed);
|
|
1104
|
+
this.log('info', 'Attempting to pause playback.');
|
|
1052
1105
|
const adapter = this.#getActiveAdapter();
|
|
1053
1106
|
if (!adapter)
|
|
1054
1107
|
return;
|
|
@@ -1057,6 +1110,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1057
1110
|
}
|
|
1058
1111
|
await adapter.pause();
|
|
1059
1112
|
this.#isPlaying = false;
|
|
1113
|
+
this.log('info', 'Playback paused.');
|
|
1060
1114
|
this.emit('pause', this.#currentIndex);
|
|
1061
1115
|
}
|
|
1062
1116
|
/**
|
|
@@ -1065,6 +1119,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1065
1119
|
*/
|
|
1066
1120
|
async stop() {
|
|
1067
1121
|
checkDestroy(this.#destroyed);
|
|
1122
|
+
this.log('info', 'Attempting to stop playback.');
|
|
1068
1123
|
const adapter = this.#getActiveAdapter();
|
|
1069
1124
|
if (!adapter)
|
|
1070
1125
|
return;
|
|
@@ -1073,6 +1128,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1073
1128
|
}
|
|
1074
1129
|
await adapter.stop();
|
|
1075
1130
|
this.#isPlaying = false;
|
|
1131
|
+
this.log('info', 'Playback stopped.');
|
|
1076
1132
|
this.emit('stop', this.#currentIndex);
|
|
1077
1133
|
}
|
|
1078
1134
|
/**
|
|
@@ -1083,23 +1139,27 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1083
1139
|
checkDestroy(this.#destroyed);
|
|
1084
1140
|
if (this.#playlist.length === 0)
|
|
1085
1141
|
return;
|
|
1142
|
+
this.log('info', 'Next track requested.');
|
|
1086
1143
|
await this.stop();
|
|
1087
1144
|
if (this.#loopMode === 'TRACK') {
|
|
1145
|
+
this.log('info', 'Loop mode: TRACK. Repeating current track.');
|
|
1088
1146
|
// Index remains the same
|
|
1089
1147
|
await this.play();
|
|
1090
1148
|
return;
|
|
1091
1149
|
}
|
|
1092
1150
|
if (this.#isRandom) {
|
|
1093
1151
|
this.#currentIndex = this._getWeightedRandomIndex();
|
|
1152
|
+
this.log('info', `Random mode: Selected index ${this.#currentIndex}`);
|
|
1094
1153
|
}
|
|
1095
1154
|
else {
|
|
1096
1155
|
let nextIndex = this.#currentIndex + 1;
|
|
1097
1156
|
if (nextIndex >= this.#playlist.length) {
|
|
1098
1157
|
if (this.#loopMode === 'PLAYLIST') {
|
|
1099
1158
|
nextIndex = 0;
|
|
1159
|
+
this.log('info', 'Loop mode: PLAYLIST. Returning to start.');
|
|
1100
1160
|
}
|
|
1101
1161
|
else {
|
|
1102
|
-
|
|
1162
|
+
this.log('info', 'End of playlist reached. Stopping.');
|
|
1103
1163
|
return;
|
|
1104
1164
|
}
|
|
1105
1165
|
}
|
|
@@ -1118,6 +1178,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1118
1178
|
return;
|
|
1119
1179
|
// UX: Repeat current track if "repeat on prev" is enabled and it's the first click
|
|
1120
1180
|
if (this.#repeatCurrentOnPrev && !this.#prevClickedToRepeat) {
|
|
1181
|
+
this.log('info', 'UX: Repeat current track on first "prev" click.');
|
|
1121
1182
|
this.#prevClickedToRepeat = true;
|
|
1122
1183
|
// Reset the "repeat" state after 2 seconds of inactivity to prevent glitches
|
|
1123
1184
|
if (this.#prevClickTimer)
|
|
@@ -1136,18 +1197,21 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1136
1197
|
this.#prevClickTimer = null;
|
|
1137
1198
|
}
|
|
1138
1199
|
this.#prevClickedToRepeat = false;
|
|
1200
|
+
this.log('info', 'Previous track requested.');
|
|
1139
1201
|
await this.stop();
|
|
1140
1202
|
if (this.#isRandom) {
|
|
1141
1203
|
this.#currentIndex = this._getWeightedRandomIndex();
|
|
1204
|
+
this.log('info', `Random mode: Selected index ${this.#currentIndex}`);
|
|
1142
1205
|
}
|
|
1143
1206
|
else {
|
|
1144
1207
|
let prevIndex = this.#currentIndex - 1;
|
|
1145
1208
|
if (prevIndex < 0) {
|
|
1146
1209
|
if (this.#loopMode === 'PLAYLIST') {
|
|
1147
1210
|
prevIndex = this.#playlist.length - 1;
|
|
1211
|
+
this.log('info', 'Loop mode: PLAYLIST. Returning to end.');
|
|
1148
1212
|
}
|
|
1149
1213
|
else {
|
|
1150
|
-
|
|
1214
|
+
this.log('info', 'Start of playlist reached. Stopping.');
|
|
1151
1215
|
return;
|
|
1152
1216
|
}
|
|
1153
1217
|
}
|
|
@@ -1156,6 +1220,27 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1156
1220
|
this.emit('trackChange', this.#currentIndex);
|
|
1157
1221
|
await this.play();
|
|
1158
1222
|
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Jumps directly to a specific index in the playlist.
|
|
1225
|
+
* @param {number} index - The target index to skip to.
|
|
1226
|
+
* @returns {Promise<void>}
|
|
1227
|
+
* @throws {TypeError} If the index is not a number.
|
|
1228
|
+
* @throws {RangeError} If the index is out of the playlist bounds.
|
|
1229
|
+
*/
|
|
1230
|
+
async skipTo(index) {
|
|
1231
|
+
checkDestroy(this.#destroyed);
|
|
1232
|
+
if (typeof index !== 'number') {
|
|
1233
|
+
throw new TypeError('Index must be a number.');
|
|
1234
|
+
}
|
|
1235
|
+
if (!this.existsTrack(index)) {
|
|
1236
|
+
throw new RangeError(`Index ${index} is out of bounds for the current playlist.`);
|
|
1237
|
+
}
|
|
1238
|
+
this.log('info', `Skipping to index: ${index}`);
|
|
1239
|
+
await this.stop();
|
|
1240
|
+
this.#currentIndex = index;
|
|
1241
|
+
this.emit('trackChange', this.#currentIndex);
|
|
1242
|
+
await this.play();
|
|
1243
|
+
}
|
|
1159
1244
|
/**
|
|
1160
1245
|
* Jumps to a specific absolute time in the timeline.
|
|
1161
1246
|
* @param {number} timeMs - The target time in milliseconds.
|
|
@@ -1171,6 +1256,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1171
1256
|
if (timeMs < 0) {
|
|
1172
1257
|
throw new RangeError('Seek time cannot be negative.');
|
|
1173
1258
|
}
|
|
1259
|
+
this.log('info', `Seeking to: ${timeMs}ms`);
|
|
1174
1260
|
const adapter = this.#getActiveAdapter();
|
|
1175
1261
|
if (adapter) {
|
|
1176
1262
|
await adapter.seek(timeMs);
|
|
@@ -1188,6 +1274,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1188
1274
|
if (typeof stepMs !== 'number') {
|
|
1189
1275
|
throw new TypeError('Step amount must be a number in milliseconds.');
|
|
1190
1276
|
}
|
|
1277
|
+
this.log('info', `Stepping by: ${stepMs}ms`);
|
|
1191
1278
|
const adapter = this.#getActiveAdapter();
|
|
1192
1279
|
if (!adapter)
|
|
1193
1280
|
return;
|
|
@@ -1257,6 +1344,34 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1257
1344
|
return 0;
|
|
1258
1345
|
}
|
|
1259
1346
|
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Checks whether the media adapter is paused.
|
|
1349
|
+
* @returns {boolean} True if the adapter is ready, false otherwise.
|
|
1350
|
+
*/
|
|
1351
|
+
get isPaused() {
|
|
1352
|
+
checkDestroy(this.#destroyed);
|
|
1353
|
+
try {
|
|
1354
|
+
const adapter = this.#getActiveAdapter();
|
|
1355
|
+
return adapter ? adapter.isPaused() : false;
|
|
1356
|
+
}
|
|
1357
|
+
catch {
|
|
1358
|
+
return false;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Checks whether the media adapter is ended.
|
|
1363
|
+
* @returns {boolean} True if the adapter is ready, false otherwise.
|
|
1364
|
+
*/
|
|
1365
|
+
get isEnded() {
|
|
1366
|
+
checkDestroy(this.#destroyed);
|
|
1367
|
+
try {
|
|
1368
|
+
const adapter = this.#getActiveAdapter();
|
|
1369
|
+
return adapter ? adapter.isEnded() : true;
|
|
1370
|
+
}
|
|
1371
|
+
catch {
|
|
1372
|
+
return true;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1260
1375
|
/**
|
|
1261
1376
|
* Retrieves the metadata of the currently loaded content, returning a structured
|
|
1262
1377
|
* object containing details such as the content ID, title, author, and duration.
|
|
@@ -1308,6 +1423,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1308
1423
|
async destroy() {
|
|
1309
1424
|
if (this.#destroyed)
|
|
1310
1425
|
return;
|
|
1426
|
+
this.log('info', 'Destroying TinyMediaPlayer instance.');
|
|
1311
1427
|
// 1. Stop current playback if active to halt media processes
|
|
1312
1428
|
try {
|
|
1313
1429
|
if (this.#isPlaying) {
|
|
@@ -1315,6 +1431,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1315
1431
|
}
|
|
1316
1432
|
}
|
|
1317
1433
|
catch (error) {
|
|
1434
|
+
this.log('error', 'Non-fatal error during playback termination:', error);
|
|
1318
1435
|
console.warn('[TinyMediaPlayer] Non-fatal error during playback termination:', error);
|
|
1319
1436
|
}
|
|
1320
1437
|
if (this.#prevClickTimer)
|
|
@@ -182,9 +182,10 @@ class TinyPromiseQueue {
|
|
|
182
182
|
* Inserts a point in the queue where subsequent tasks will be grouped and executed together in a Promise.all.
|
|
183
183
|
* If the queue is currently empty, behaves like a regular promise.
|
|
184
184
|
*
|
|
185
|
-
* @
|
|
185
|
+
* @template {any} PromiseResult
|
|
186
|
+
* @param {(...args: any[]) => Promise<PromiseResult>} task A function that returns a Promise.
|
|
186
187
|
* @param {string} [id] Optional ID to identify the task in the queue.
|
|
187
|
-
* @returns {Promise<
|
|
188
|
+
* @returns {Promise<PromiseResult>} A Promise that resolves or rejects with the result of the task once it's processed.
|
|
188
189
|
* @throws {Error} Throws if param is invalid.
|
|
189
190
|
*/
|
|
190
191
|
async enqueuePoint(task, id) {
|
|
@@ -206,10 +207,11 @@ class TinyPromiseQueue {
|
|
|
206
207
|
* If the task is canceled before execution, it will be rejected with the message:
|
|
207
208
|
* "The function was canceled on TinyPromiseQueue."
|
|
208
209
|
*
|
|
209
|
-
* @
|
|
210
|
+
* @template {any} PromiseResult
|
|
211
|
+
* @param {(...args: any[]) => Promise<PromiseResult>} task A function that returns a Promise to be executed sequentially.
|
|
210
212
|
* @param {number|null} [delay] Optional delay (in ms) before the task is executed.
|
|
211
213
|
* @param {string} [id] Optional ID to identify the task in the queue.
|
|
212
|
-
* @returns {Promise<
|
|
214
|
+
* @returns {Promise<PromiseResult>} A Promise that resolves or rejects with the result of the task once it's processed.
|
|
213
215
|
* @throws {Error} Throws if param is invalid.
|
|
214
216
|
*/
|
|
215
217
|
enqueue(task, delay, id) {
|
|
@@ -76,12 +76,13 @@ declare class TinyPromiseQueue {
|
|
|
76
76
|
* Inserts a point in the queue where subsequent tasks will be grouped and executed together in a Promise.all.
|
|
77
77
|
* If the queue is currently empty, behaves like a regular promise.
|
|
78
78
|
*
|
|
79
|
-
* @
|
|
79
|
+
* @template {any} PromiseResult
|
|
80
|
+
* @param {(...args: any[]) => Promise<PromiseResult>} task A function that returns a Promise.
|
|
80
81
|
* @param {string} [id] Optional ID to identify the task in the queue.
|
|
81
|
-
* @returns {Promise<
|
|
82
|
+
* @returns {Promise<PromiseResult>} A Promise that resolves or rejects with the result of the task once it's processed.
|
|
82
83
|
* @throws {Error} Throws if param is invalid.
|
|
83
84
|
*/
|
|
84
|
-
enqueuePoint(task: (...args: any[]) => Promise<
|
|
85
|
+
enqueuePoint<PromiseResult extends any>(task: (...args: any[]) => Promise<PromiseResult>, id?: string): Promise<PromiseResult>;
|
|
85
86
|
/**
|
|
86
87
|
* Adds a new async task to the queue and ensures it runs in order after previous tasks.
|
|
87
88
|
* Optionally, a delay can be added before the task is executed.
|
|
@@ -89,13 +90,14 @@ declare class TinyPromiseQueue {
|
|
|
89
90
|
* If the task is canceled before execution, it will be rejected with the message:
|
|
90
91
|
* "The function was canceled on TinyPromiseQueue."
|
|
91
92
|
*
|
|
92
|
-
* @
|
|
93
|
+
* @template {any} PromiseResult
|
|
94
|
+
* @param {(...args: any[]) => Promise<PromiseResult>} task A function that returns a Promise to be executed sequentially.
|
|
93
95
|
* @param {number|null} [delay] Optional delay (in ms) before the task is executed.
|
|
94
96
|
* @param {string} [id] Optional ID to identify the task in the queue.
|
|
95
|
-
* @returns {Promise<
|
|
97
|
+
* @returns {Promise<PromiseResult>} A Promise that resolves or rejects with the result of the task once it's processed.
|
|
96
98
|
* @throws {Error} Throws if param is invalid.
|
|
97
99
|
*/
|
|
98
|
-
enqueue(task: (...args: any[]) => Promise<
|
|
100
|
+
enqueue<PromiseResult extends any>(task: (...args: any[]) => Promise<PromiseResult>, delay?: number | null, id?: string): Promise<PromiseResult>;
|
|
99
101
|
/**
|
|
100
102
|
* Cancels a scheduled delay and removes the task from the queue.
|
|
101
103
|
* Adds the ID to a blacklist so the task is skipped if already being processed.
|
|
@@ -162,9 +162,10 @@ class TinyPromiseQueue {
|
|
|
162
162
|
* Inserts a point in the queue where subsequent tasks will be grouped and executed together in a Promise.all.
|
|
163
163
|
* If the queue is currently empty, behaves like a regular promise.
|
|
164
164
|
*
|
|
165
|
-
* @
|
|
165
|
+
* @template {any} PromiseResult
|
|
166
|
+
* @param {(...args: any[]) => Promise<PromiseResult>} task A function that returns a Promise.
|
|
166
167
|
* @param {string} [id] Optional ID to identify the task in the queue.
|
|
167
|
-
* @returns {Promise<
|
|
168
|
+
* @returns {Promise<PromiseResult>} A Promise that resolves or rejects with the result of the task once it's processed.
|
|
168
169
|
* @throws {Error} Throws if param is invalid.
|
|
169
170
|
*/
|
|
170
171
|
async enqueuePoint(task, id) {
|
|
@@ -186,10 +187,11 @@ class TinyPromiseQueue {
|
|
|
186
187
|
* If the task is canceled before execution, it will be rejected with the message:
|
|
187
188
|
* "The function was canceled on TinyPromiseQueue."
|
|
188
189
|
*
|
|
189
|
-
* @
|
|
190
|
+
* @template {any} PromiseResult
|
|
191
|
+
* @param {(...args: any[]) => Promise<PromiseResult>} task A function that returns a Promise to be executed sequentially.
|
|
190
192
|
* @param {number|null} [delay] Optional delay (in ms) before the task is executed.
|
|
191
193
|
* @param {string} [id] Optional ID to identify the task in the queue.
|
|
192
|
-
* @returns {Promise<
|
|
194
|
+
* @returns {Promise<PromiseResult>} A Promise that resolves or rejects with the result of the task once it's processed.
|
|
193
195
|
* @throws {Error} Throws if param is invalid.
|
|
194
196
|
*/
|
|
195
197
|
enqueue(task, delay, id) {
|