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.
Files changed (55) hide show
  1. package/changelog/1/28/9.md +17 -0
  2. package/dist/v1/TinyBasicsEs.min.js +1 -1
  3. package/dist/v1/TinyDebugger.min.js +1 -0
  4. package/dist/v1/TinyEssentials.min.js +1 -1
  5. package/dist/v1/TinyMediaPlayer.min.js +1 -1
  6. package/dist/v1/TinyRadioFm.min.js +1 -1
  7. package/dist/v1/TinyUploadClicker.min.js +1 -1
  8. package/dist/v1/basics/browserDetector.cjs +228 -0
  9. package/dist/v1/basics/browserDetector.d.mts +66 -0
  10. package/dist/v1/basics/browserDetector.mjs +207 -0
  11. package/dist/v1/basics/index.cjs +6 -0
  12. package/dist/v1/basics/index.d.mts +2 -1
  13. package/dist/v1/basics/index.mjs +2 -1
  14. package/dist/v1/build/TinyDebugger.cjs +7 -0
  15. package/dist/v1/build/TinyDebugger.d.mts +3 -0
  16. package/dist/v1/build/TinyDebugger.mjs +2 -0
  17. package/dist/v1/index.cjs +8 -0
  18. package/dist/v1/index.d.mts +3 -1
  19. package/dist/v1/index.mjs +4 -2
  20. package/dist/v1/libs/TinyDebugger.cjs +380 -0
  21. package/dist/v1/libs/TinyDebugger.d.mts +132 -0
  22. package/dist/v1/libs/TinyDebugger.mjs +351 -0
  23. package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.cjs +39 -3
  24. package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.d.mts +10 -0
  25. package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.mjs +35 -3
  26. package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.cjs +39 -3
  27. package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.d.mts +10 -0
  28. package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.mjs +35 -3
  29. package/dist/v1/libs/TinyMediaPlayer/Mock.cjs +333 -0
  30. package/dist/v1/libs/TinyMediaPlayer/Mock.d.mts +154 -0
  31. package/dist/v1/libs/TinyMediaPlayer/Mock.mjs +296 -0
  32. package/dist/v1/libs/TinyMediaPlayer/SoundCloud.cjs +30 -0
  33. package/dist/v1/libs/TinyMediaPlayer/SoundCloud.d.mts +10 -0
  34. package/dist/v1/libs/TinyMediaPlayer/SoundCloud.mjs +26 -0
  35. package/dist/v1/libs/TinyMediaPlayer/Youtube.cjs +39 -3
  36. package/dist/v1/libs/TinyMediaPlayer/Youtube.d.mts +10 -0
  37. package/dist/v1/libs/TinyMediaPlayer/Youtube.mjs +32 -3
  38. package/dist/v1/libs/TinyMediaPlayer/index.cjs +18 -0
  39. package/dist/v1/libs/TinyMediaPlayer/index.d.mts +10 -0
  40. package/dist/v1/libs/TinyMediaPlayer/index.mjs +16 -0
  41. package/dist/v1/libs/TinyMediaPlayer.cjs +156 -31
  42. package/dist/v1/libs/TinyMediaPlayer.d.mts +43 -7
  43. package/dist/v1/libs/TinyMediaPlayer.mjs +148 -31
  44. package/dist/v1/libs/TinyPromiseQueue.cjs +6 -4
  45. package/dist/v1/libs/TinyPromiseQueue.d.mts +8 -6
  46. package/dist/v1/libs/TinyPromiseQueue.mjs +6 -4
  47. package/dist/v1/libs/TinyRadioFm.cjs +271 -138
  48. package/dist/v1/libs/TinyRadioFm.d.mts +67 -3
  49. package/dist/v1/libs/TinyRadioFm.mjs +239 -123
  50. package/docs/v1/README.md +2 -0
  51. package/docs/v1/basics/browserDetector.md +135 -0
  52. package/docs/v1/libs/TinyDebugger.md +119 -0
  53. package/docs/v1/libs/TinyMediaPlayer.md +26 -22
  54. package/docs/v1/libs/TinyRadioFm.md +25 -3
  55. package/package.json +13 -1
@@ -1,4 +1,4 @@
1
- import { EventEmitter } from 'events';
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 EventEmitter {
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
- * Sets the mute state of the player.
620
- * This state is synchronized with the active adapter and respected when playing new tracks.
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
- set isMuted(value) {
646
+ async mute() {
625
647
  checkDestroy(this.#destroyed);
626
- if (typeof value !== 'boolean') {
627
- throw new TypeError('isMuted must be a boolean.');
628
- }
629
- this.#isMuted = value;
630
- // Synchronize with the active adapter immediately
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
- throw new Error(`No compatible adapter found for content ID: ${currentContent.id}.`);
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
- await adapter.play(this.#playlist[this.#currentIndex]);
1041
- if (this.#isMuted)
1042
- await adapter.mute();
1043
- this.#isPlaying = true;
1044
- this.emit('play', this.#currentIndex);
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
- // End of playlist, stop playing
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
- // End of playlist, stop playing
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
- * @param {(...args: any[]) => Promise<any>|Promise<any>} task A function that returns a Promise.
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<any>} A Promise that resolves or rejects with the result of the task once it's processed.
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
- * @param {(...args: any[]) => Promise<any>|Promise<any>} task A function that returns a Promise to be executed sequentially.
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<any>} A Promise that resolves or rejects with the result of the task once it's processed.
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
- * @param {(...args: any[]) => Promise<any>|Promise<any>} task A function that returns a Promise.
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<any>} A Promise that resolves or rejects with the result of the task once it's processed.
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<any> | Promise<any>, id?: string): Promise<any>;
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
- * @param {(...args: any[]) => Promise<any>|Promise<any>} task A function that returns a Promise to be executed sequentially.
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<any>} A Promise that resolves or rejects with the result of the task once it's processed.
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<any> | Promise<any>, delay?: number | null, id?: string): Promise<any>;
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
- * @param {(...args: any[]) => Promise<any>|Promise<any>} task A function that returns a Promise.
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<any>} A Promise that resolves or rejects with the result of the task once it's processed.
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
- * @param {(...args: any[]) => Promise<any>|Promise<any>} task A function that returns a Promise to be executed sequentially.
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<any>} A Promise that resolves or rejects with the result of the task once it's processed.
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) {