tiny-essentials 1.28.6 → 1.28.8

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 (54) hide show
  1. package/changelog/1/28/6.md +3 -0
  2. package/changelog/1/28/7.md +7 -0
  3. package/changelog/1/28/8.md +17 -0
  4. package/dist/v1/TinyBasicsEs.min.js +1 -1
  5. package/dist/v1/TinyDebugger.min.js +1 -0
  6. package/dist/v1/TinyEssentials.min.js +1 -1
  7. package/dist/v1/TinyMediaPlayer.min.js +1 -1
  8. package/dist/v1/TinyRadioFm.min.js +1 -1
  9. package/dist/v1/TinyUploadClicker.min.js +1 -1
  10. package/dist/v1/basics/browserDetector.cjs +228 -0
  11. package/dist/v1/basics/browserDetector.d.mts +66 -0
  12. package/dist/v1/basics/browserDetector.mjs +207 -0
  13. package/dist/v1/basics/index.cjs +6 -0
  14. package/dist/v1/basics/index.d.mts +2 -1
  15. package/dist/v1/basics/index.mjs +2 -1
  16. package/dist/v1/build/TinyDebugger.cjs +7 -0
  17. package/dist/v1/build/TinyDebugger.d.mts +3 -0
  18. package/dist/v1/build/TinyDebugger.mjs +2 -0
  19. package/dist/v1/index.cjs +8 -0
  20. package/dist/v1/index.d.mts +3 -1
  21. package/dist/v1/index.mjs +4 -2
  22. package/dist/v1/libs/TinyDebugger.cjs +380 -0
  23. package/dist/v1/libs/TinyDebugger.d.mts +132 -0
  24. package/dist/v1/libs/TinyDebugger.mjs +351 -0
  25. package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.cjs +66 -3
  26. package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.d.mts +25 -0
  27. package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.mjs +59 -3
  28. package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.cjs +66 -3
  29. package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.d.mts +25 -0
  30. package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.mjs +59 -3
  31. package/dist/v1/libs/TinyMediaPlayer/Mock.cjs +333 -0
  32. package/dist/v1/libs/TinyMediaPlayer/Mock.d.mts +154 -0
  33. package/dist/v1/libs/TinyMediaPlayer/Mock.mjs +296 -0
  34. package/dist/v1/libs/TinyMediaPlayer/SoundCloud.cjs +85 -1
  35. package/dist/v1/libs/TinyMediaPlayer/SoundCloud.d.mts +25 -0
  36. package/dist/v1/libs/TinyMediaPlayer/SoundCloud.mjs +75 -1
  37. package/dist/v1/libs/TinyMediaPlayer/Youtube.cjs +66 -3
  38. package/dist/v1/libs/TinyMediaPlayer/Youtube.d.mts +25 -0
  39. package/dist/v1/libs/TinyMediaPlayer/Youtube.mjs +56 -3
  40. package/dist/v1/libs/TinyMediaPlayer/index.cjs +45 -0
  41. package/dist/v1/libs/TinyMediaPlayer/index.d.mts +25 -0
  42. package/dist/v1/libs/TinyMediaPlayer/index.mjs +40 -0
  43. package/dist/v1/libs/TinyMediaPlayer.cjs +172 -10
  44. package/dist/v1/libs/TinyMediaPlayer.d.mts +47 -2
  45. package/dist/v1/libs/TinyMediaPlayer.mjs +160 -10
  46. package/dist/v1/libs/TinyPromiseQueue.cjs +6 -4
  47. package/dist/v1/libs/TinyPromiseQueue.d.mts +8 -6
  48. package/dist/v1/libs/TinyPromiseQueue.mjs +6 -4
  49. package/dist/v1/libs/TinyRadioFm.cjs +271 -138
  50. package/dist/v1/libs/TinyRadioFm.d.mts +67 -3
  51. package/dist/v1/libs/TinyRadioFm.mjs +239 -123
  52. package/docs/v1/libs/TinyMediaPlayer.md +52 -54
  53. package/docs/v1/libs/TinyRadioFm.md +25 -3
  54. 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}
@@ -98,6 +101,8 @@ class TinyMediaPlayer extends EventEmitter {
98
101
  #isRandom = false;
99
102
  /** @type {boolean} */
100
103
  #isPlaying = false;
104
+ /** @type {boolean} */
105
+ #isMuted = false;
101
106
  /** @type {number} */
102
107
  #volume = 1.0;
103
108
  /** @type {boolean} */
@@ -216,10 +221,16 @@ class TinyMediaPlayer extends EventEmitter {
216
221
  * @throws {RangeError} If numeric options are out of valid ranges.
217
222
  */
218
223
  constructor(options = {}) {
219
- super();
220
224
  if (!isValidObj(options)) {
221
225
  throw new TypeError('Options must be a non-null object.');
222
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
+ });
223
234
  // Volume configuration
224
235
  if (options.persistVolume !== undefined) {
225
236
  if (typeof options.persistVolume !== 'boolean') {
@@ -268,6 +279,7 @@ class TinyMediaPlayer extends EventEmitter {
268
279
  }
269
280
  if (this.#persistVolume)
270
281
  this.#loadVolumeFromStorage();
282
+ this.log('info', 'Instance initialized.', { options });
271
283
  }
272
284
  // ==========================================
273
285
  // STORAGE HELPERS
@@ -283,6 +295,7 @@ class TinyMediaPlayer extends EventEmitter {
283
295
  const parsedVolume = parseFloat(storedValue);
284
296
  if (!isNaN(parsedVolume) && parsedVolume >= 0 && parsedVolume <= 1) {
285
297
  this.#volume = parsedVolume;
298
+ this.log('info', `Volume loaded from storage: ${this.#volume}`);
286
299
  }
287
300
  }
288
301
  }
@@ -298,6 +311,7 @@ class TinyMediaPlayer extends EventEmitter {
298
311
  try {
299
312
  if (this.#persistVolume && typeof window !== 'undefined' && window.localStorage) {
300
313
  window.localStorage.setItem(this.#volumeStorageKey, this.#volume.toString());
314
+ this.log('info', `Volume saved to storage: ${this.#volume}`);
301
315
  }
302
316
  }
303
317
  catch (error) {
@@ -360,11 +374,14 @@ class TinyMediaPlayer extends EventEmitter {
360
374
  const startVolume = adapter.getVolume();
361
375
  const startTime = performance.now();
362
376
  const duration = this.#fadeVolumeSpeed;
377
+ this.log('info', `Starting volume fade: ${startVolume} -> ${targetVolume}`);
363
378
  return new Promise((resolve) => {
364
379
  /** @param {number} currentTime */
365
380
  const step = (currentTime) => {
366
- if (signal.aborted)
381
+ if (signal.aborted) {
382
+ this.log('info', 'Volume fade aborted.');
367
383
  return resolve();
384
+ }
368
385
  const elapsed = currentTime - startTime;
369
386
  const progress = Math.min(elapsed / duration, 1);
370
387
  const calculatedVolume = startVolume + (targetVolume - startVolume) * progress;
@@ -375,6 +392,7 @@ class TinyMediaPlayer extends EventEmitter {
375
392
  }
376
393
  else {
377
394
  adapter.setVolume(targetVolume);
395
+ this.log('info', `Volume fade completed: ${targetVolume}`);
378
396
  resolve();
379
397
  }
380
398
  };
@@ -411,6 +429,7 @@ class TinyMediaPlayer extends EventEmitter {
411
429
  }
412
430
  }
413
431
  this.#playlist = value;
432
+ this.log('info', `Playlist updated. New size: ${this.#playlist.length}`);
414
433
  this.emit('playlistUpdate', this.playlist);
415
434
  // Validation to correct currentIndex ensuring it aligns with the new playlist limits
416
435
  if (this.#playlist.length === 0) {
@@ -433,6 +452,7 @@ class TinyMediaPlayer extends EventEmitter {
433
452
  await this.play();
434
453
  }
435
454
  catch (error) {
455
+ this.log('error', 'Background transition error on playlist update:', error);
436
456
  console.warn('[TinyMediaPlayer] Background transition error on playlist update:', error);
437
457
  }
438
458
  })();
@@ -458,6 +478,7 @@ class TinyMediaPlayer extends EventEmitter {
458
478
  }
459
479
  if (this.#currentIndex === value)
460
480
  return; // Optimization: do nothing if index is the same
481
+ this.log('info', `Index change requested: ${this.#currentIndex} -> ${value}`);
461
482
  const wasPlaying = this.#isPlaying;
462
483
  let oldAdapter = null;
463
484
  if (this.#currentIndex !== -1 && this.#playlist.length > 0) {
@@ -480,6 +501,7 @@ class TinyMediaPlayer extends EventEmitter {
480
501
  await this.play();
481
502
  }
482
503
  catch (error) {
504
+ this.log('error', 'Background transition error on index update:', error);
483
505
  console.warn('[TinyMediaPlayer] Background transition error on index update:', error);
484
506
  }
485
507
  })();
@@ -501,6 +523,7 @@ class TinyMediaPlayer extends EventEmitter {
501
523
  throw new TypeError(`Loop mode must be one of: ${validModes.join(', ')}.`);
502
524
  }
503
525
  this.#loopMode = value;
526
+ this.log('info', `Loop mode changed to: ${this.#loopMode}`);
504
527
  this.emit('loopModeChange', this.#loopMode);
505
528
  }
506
529
  /** @returns {boolean} The current random mode status. */
@@ -518,6 +541,7 @@ class TinyMediaPlayer extends EventEmitter {
518
541
  throw new TypeError('Random mode state must be a boolean.');
519
542
  }
520
543
  this.#isRandom = value;
544
+ this.log('info', `Random mode: ${this.#isRandom}`);
521
545
  this.emit('randomModeChange', this.#isRandom);
522
546
  }
523
547
  /** @returns {boolean} True if media is actively playing. */
@@ -544,6 +568,7 @@ class TinyMediaPlayer extends EventEmitter {
544
568
  // Abort any ongoing fade to ensure manual control is immediate
545
569
  if (this.#fadeController)
546
570
  this.#fadeController.abort();
571
+ this.log('info', `Volume changed: ${this.#volume} -> ${value}`);
547
572
  this.#volume = value;
548
573
  this.#saveVolumeToStorage();
549
574
  this.emit('volumeChange', this.#volume);
@@ -574,6 +599,7 @@ class TinyMediaPlayer extends EventEmitter {
574
599
  throw new TypeError('Persist volume parameter must be a boolean.');
575
600
  }
576
601
  this.#persistVolume = value;
602
+ this.log('info', `Persist volume: ${this.#persistVolume}`);
577
603
  if (value) {
578
604
  this.#saveVolumeToStorage();
579
605
  }
@@ -608,6 +634,39 @@ class TinyMediaPlayer extends EventEmitter {
608
634
  console.warn('[TinyMediaPlayer] Failed to migrate volume storage key.', error);
609
635
  }
610
636
  }
637
+ /** @returns {boolean} True if the player is currently muted. */
638
+ get isMuted() {
639
+ checkDestroy(this.#destroyed);
640
+ return this.#isMuted;
641
+ }
642
+ /**
643
+ * Mutes the current playback.
644
+ * @returns {Promise<void>}
645
+ */
646
+ async mute() {
647
+ checkDestroy(this.#destroyed);
648
+ const adapter = this.#getActiveAdapter();
649
+ if (!adapter)
650
+ return;
651
+ await adapter.mute();
652
+ this.#isMuted = true;
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.');
669
+ }
611
670
  // ==========================================
612
671
  // ADAPTER MANAGEMENT
613
672
  // ==========================================
@@ -655,6 +714,7 @@ class TinyMediaPlayer extends EventEmitter {
655
714
  if (!(adapter instanceof BaseMediaAdapter)) {
656
715
  throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
657
716
  }
717
+ this.log('info', 'Registering new adapter:', adapter);
658
718
  const events = new Map();
659
719
  this.#adapterHandlers.set(adapter, events);
660
720
  // Use the controlled helper to ensure each event is registered
@@ -674,6 +734,7 @@ class TinyMediaPlayer extends EventEmitter {
674
734
  if (!(adapter instanceof BaseMediaAdapter)) {
675
735
  throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
676
736
  }
737
+ this.log('info', 'Removing adapter:', adapter);
677
738
  this.#removeHandler(adapter);
678
739
  return this.#adapters.delete(adapter);
679
740
  }
@@ -687,6 +748,7 @@ class TinyMediaPlayer extends EventEmitter {
687
748
  if (!(adapter instanceof BaseMediaAdapter)) {
688
749
  throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
689
750
  }
751
+ this.log('info', 'Destroying adapter:', adapter);
690
752
  this.#removeHandler(adapter);
691
753
  const result = this.#adapters.delete(adapter);
692
754
  if (result)
@@ -710,6 +772,7 @@ class TinyMediaPlayer extends EventEmitter {
710
772
  */
711
773
  destroyAllAdapters() {
712
774
  checkDestroy(this.#destroyed);
775
+ this.log('info', 'Destroying all adapters.');
713
776
  this.#adapters.forEach((adapter) => {
714
777
  this.#removeHandler(adapter);
715
778
  adapter.destroy();
@@ -721,6 +784,7 @@ class TinyMediaPlayer extends EventEmitter {
721
784
  */
722
785
  clearAdapters() {
723
786
  checkDestroy(this.#destroyed);
787
+ this.log('info', 'Clearing all adapters.');
724
788
  this.#adapters.forEach((adapter) => this.#removeHandler(adapter));
725
789
  this.#adapters.clear();
726
790
  }
@@ -753,7 +817,9 @@ class TinyMediaPlayer extends EventEmitter {
753
817
  const adapter = this.getMediaAdapter(currentContent);
754
818
  if (adapter)
755
819
  return adapter;
756
- 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);
757
823
  }
758
824
  // ==========================================
759
825
  // EVENT MANAGEMENT (CONTROLLED)
@@ -771,6 +837,7 @@ class TinyMediaPlayer extends EventEmitter {
771
837
  // If the event is already in the list, do nothing to avoid duplicates
772
838
  if (this.#adapterEventNames.has(eventName))
773
839
  return;
840
+ this.log('info', `Adding adapter event listener: ${eventName}`);
774
841
  this.#adapterEventNames.add(eventName);
775
842
  // Synchronize: Add the listener to all already registered adapters
776
843
  this.#adapters.forEach((adapter) => {
@@ -789,6 +856,7 @@ class TinyMediaPlayer extends EventEmitter {
789
856
  }
790
857
  if (!this.#adapterEventNames.has(eventName))
791
858
  return;
859
+ this.log('info', `Removing adapter event listener: ${eventName}`);
792
860
  this.#adapterEventNames.delete(eventName);
793
861
  // Synchronize: Remove the listener from all registered adapters
794
862
  this.#adapters.forEach((adapter) => {
@@ -830,6 +898,8 @@ class TinyMediaPlayer extends EventEmitter {
830
898
  /** @type {(...args: any[]) => any} */
831
899
  const handler = (...args) => {
832
900
  if (!this.#destroyed) {
901
+ if (eventName !== 'timeupdate')
902
+ this.log('debug', `Adapter event triggered: ${eventName}`, ...args);
833
903
  this.emit(eventName, ...args);
834
904
  }
835
905
  };
@@ -869,6 +939,7 @@ class TinyMediaPlayer extends EventEmitter {
869
939
  const newContent = { ...getMediaContentBase(), ...getMediaContentMetadata(), ...content };
870
940
  valMediaContentMetadata(newContent);
871
941
  const newLength = this.#playlist.push(newContent);
942
+ this.log('info', `Track added. New length: ${newLength}`, newContent);
872
943
  if (this.#currentIndex === -1) {
873
944
  this.#currentIndex = 0;
874
945
  this.emit('trackChange', this.#currentIndex);
@@ -921,6 +992,7 @@ class TinyMediaPlayer extends EventEmitter {
921
992
  if (!this.existsTrack(index)) {
922
993
  throw new RangeError(`Index ${index} is out of bounds for the current playlist.`);
923
994
  }
995
+ this.log('info', `Removing track at index: ${index}`);
924
996
  if (index === this.#currentIndex) {
925
997
  // Stop the current track if it is the one being removed
926
998
  await this.stop();
@@ -956,6 +1028,7 @@ class TinyMediaPlayer extends EventEmitter {
956
1028
  if (typeof query !== 'string' && typeof query !== 'function') {
957
1029
  throw new TypeError('Search query must be a string or a boolean evaluation function.');
958
1030
  }
1031
+ this.log('info', 'Searching playlist...', { query });
959
1032
  const results = [];
960
1033
  for (let i = 0; i < this.#playlist.length; i++) {
961
1034
  const track = this.#playlist[i];
@@ -974,6 +1047,7 @@ class TinyMediaPlayer extends EventEmitter {
974
1047
  results.push({ track, index: i });
975
1048
  }
976
1049
  }
1050
+ this.log('info', `Search completed. Found ${results.length} results.`);
977
1051
  return results;
978
1052
  }
979
1053
  /**
@@ -981,6 +1055,7 @@ class TinyMediaPlayer extends EventEmitter {
981
1055
  */
982
1056
  async clearPlaylist() {
983
1057
  checkDestroy(this.#destroyed);
1058
+ this.log('info', 'Clearing entire playlist.');
984
1059
  await this.stop();
985
1060
  this.#playlist = [];
986
1061
  this.#currentIndex = -1;
@@ -996,6 +1071,7 @@ class TinyMediaPlayer extends EventEmitter {
996
1071
  */
997
1072
  async play() {
998
1073
  checkDestroy(this.#destroyed);
1074
+ this.log('info', `Attempting to play track at index: ${this.#currentIndex}`);
999
1075
  const adapter = this.#getActiveAdapter();
1000
1076
  if (!adapter)
1001
1077
  return;
@@ -1006,9 +1082,18 @@ class TinyMediaPlayer extends EventEmitter {
1006
1082
  else {
1007
1083
  adapter.setVolume(this.#volume);
1008
1084
  }
1009
- await adapter.play(this.#playlist[this.#currentIndex]);
1010
- this.#isPlaying = true;
1011
- 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
+ }
1012
1097
  }
1013
1098
  /**
1014
1099
  * Pauses the current track.
@@ -1016,6 +1101,7 @@ class TinyMediaPlayer extends EventEmitter {
1016
1101
  */
1017
1102
  async pause() {
1018
1103
  checkDestroy(this.#destroyed);
1104
+ this.log('info', 'Attempting to pause playback.');
1019
1105
  const adapter = this.#getActiveAdapter();
1020
1106
  if (!adapter)
1021
1107
  return;
@@ -1024,6 +1110,7 @@ class TinyMediaPlayer extends EventEmitter {
1024
1110
  }
1025
1111
  await adapter.pause();
1026
1112
  this.#isPlaying = false;
1113
+ this.log('info', 'Playback paused.');
1027
1114
  this.emit('pause', this.#currentIndex);
1028
1115
  }
1029
1116
  /**
@@ -1032,6 +1119,7 @@ class TinyMediaPlayer extends EventEmitter {
1032
1119
  */
1033
1120
  async stop() {
1034
1121
  checkDestroy(this.#destroyed);
1122
+ this.log('info', 'Attempting to stop playback.');
1035
1123
  const adapter = this.#getActiveAdapter();
1036
1124
  if (!adapter)
1037
1125
  return;
@@ -1040,6 +1128,7 @@ class TinyMediaPlayer extends EventEmitter {
1040
1128
  }
1041
1129
  await adapter.stop();
1042
1130
  this.#isPlaying = false;
1131
+ this.log('info', 'Playback stopped.');
1043
1132
  this.emit('stop', this.#currentIndex);
1044
1133
  }
1045
1134
  /**
@@ -1050,23 +1139,27 @@ class TinyMediaPlayer extends EventEmitter {
1050
1139
  checkDestroy(this.#destroyed);
1051
1140
  if (this.#playlist.length === 0)
1052
1141
  return;
1142
+ this.log('info', 'Next track requested.');
1053
1143
  await this.stop();
1054
1144
  if (this.#loopMode === 'TRACK') {
1145
+ this.log('info', 'Loop mode: TRACK. Repeating current track.');
1055
1146
  // Index remains the same
1056
1147
  await this.play();
1057
1148
  return;
1058
1149
  }
1059
1150
  if (this.#isRandom) {
1060
1151
  this.#currentIndex = this._getWeightedRandomIndex();
1152
+ this.log('info', `Random mode: Selected index ${this.#currentIndex}`);
1061
1153
  }
1062
1154
  else {
1063
1155
  let nextIndex = this.#currentIndex + 1;
1064
1156
  if (nextIndex >= this.#playlist.length) {
1065
1157
  if (this.#loopMode === 'PLAYLIST') {
1066
1158
  nextIndex = 0;
1159
+ this.log('info', 'Loop mode: PLAYLIST. Returning to start.');
1067
1160
  }
1068
1161
  else {
1069
- // End of playlist, stop playing
1162
+ this.log('info', 'End of playlist reached. Stopping.');
1070
1163
  return;
1071
1164
  }
1072
1165
  }
@@ -1085,6 +1178,7 @@ class TinyMediaPlayer extends EventEmitter {
1085
1178
  return;
1086
1179
  // UX: Repeat current track if "repeat on prev" is enabled and it's the first click
1087
1180
  if (this.#repeatCurrentOnPrev && !this.#prevClickedToRepeat) {
1181
+ this.log('info', 'UX: Repeat current track on first "prev" click.');
1088
1182
  this.#prevClickedToRepeat = true;
1089
1183
  // Reset the "repeat" state after 2 seconds of inactivity to prevent glitches
1090
1184
  if (this.#prevClickTimer)
@@ -1103,18 +1197,21 @@ class TinyMediaPlayer extends EventEmitter {
1103
1197
  this.#prevClickTimer = null;
1104
1198
  }
1105
1199
  this.#prevClickedToRepeat = false;
1200
+ this.log('info', 'Previous track requested.');
1106
1201
  await this.stop();
1107
1202
  if (this.#isRandom) {
1108
1203
  this.#currentIndex = this._getWeightedRandomIndex();
1204
+ this.log('info', `Random mode: Selected index ${this.#currentIndex}`);
1109
1205
  }
1110
1206
  else {
1111
1207
  let prevIndex = this.#currentIndex - 1;
1112
1208
  if (prevIndex < 0) {
1113
1209
  if (this.#loopMode === 'PLAYLIST') {
1114
1210
  prevIndex = this.#playlist.length - 1;
1211
+ this.log('info', 'Loop mode: PLAYLIST. Returning to end.');
1115
1212
  }
1116
1213
  else {
1117
- // End of playlist, stop playing
1214
+ this.log('info', 'Start of playlist reached. Stopping.');
1118
1215
  return;
1119
1216
  }
1120
1217
  }
@@ -1123,6 +1220,27 @@ class TinyMediaPlayer extends EventEmitter {
1123
1220
  this.emit('trackChange', this.#currentIndex);
1124
1221
  await this.play();
1125
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
+ }
1126
1244
  /**
1127
1245
  * Jumps to a specific absolute time in the timeline.
1128
1246
  * @param {number} timeMs - The target time in milliseconds.
@@ -1138,6 +1256,7 @@ class TinyMediaPlayer extends EventEmitter {
1138
1256
  if (timeMs < 0) {
1139
1257
  throw new RangeError('Seek time cannot be negative.');
1140
1258
  }
1259
+ this.log('info', `Seeking to: ${timeMs}ms`);
1141
1260
  const adapter = this.#getActiveAdapter();
1142
1261
  if (adapter) {
1143
1262
  await adapter.seek(timeMs);
@@ -1155,6 +1274,7 @@ class TinyMediaPlayer extends EventEmitter {
1155
1274
  if (typeof stepMs !== 'number') {
1156
1275
  throw new TypeError('Step amount must be a number in milliseconds.');
1157
1276
  }
1277
+ this.log('info', `Stepping by: ${stepMs}ms`);
1158
1278
  const adapter = this.#getActiveAdapter();
1159
1279
  if (!adapter)
1160
1280
  return;
@@ -1224,6 +1344,34 @@ class TinyMediaPlayer extends EventEmitter {
1224
1344
  return 0;
1225
1345
  }
1226
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
+ }
1227
1375
  /**
1228
1376
  * Retrieves the metadata of the currently loaded content, returning a structured
1229
1377
  * object containing details such as the content ID, title, author, and duration.
@@ -1275,6 +1423,7 @@ class TinyMediaPlayer extends EventEmitter {
1275
1423
  async destroy() {
1276
1424
  if (this.#destroyed)
1277
1425
  return;
1426
+ this.log('info', 'Destroying TinyMediaPlayer instance.');
1278
1427
  // 1. Stop current playback if active to halt media processes
1279
1428
  try {
1280
1429
  if (this.#isPlaying) {
@@ -1282,6 +1431,7 @@ class TinyMediaPlayer extends EventEmitter {
1282
1431
  }
1283
1432
  }
1284
1433
  catch (error) {
1434
+ this.log('error', 'Non-fatal error during playback termination:', error);
1285
1435
  console.warn('[TinyMediaPlayer] Non-fatal error during playback termination:', error);
1286
1436
  }
1287
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) {