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,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var events = require('events');
3
+ var TinyDebugger = require('./TinyDebugger.cjs');
4
4
  var mediaContent = require('../basics/mediaContent.cjs');
5
5
  var utils = require('./utils.cjs');
6
6
  var index = require('./TinyMediaPlayer/index.cjs');
@@ -34,6 +34,9 @@ const checkDestroy = utils.createCheckDestroyed('TinyMediaPlayer');
34
34
  * @property {boolean} [repeatCurrentOnPrev=false] - If true, clicking 'previous' repeats the current track on the first click.
35
35
  * @property {boolean} [smoothPlayPauseVolume=false] - If true, volume fades smoothly during play/pause transitions.
36
36
  * @property {boolean} [smoothStopVolume=false] - If true, volume fades smoothly to zero when stopping.
37
+ * @property {boolean} [debugMode=false] - Whether to enable internal debug logging.
38
+ * @property {boolean} [useLogColors=false] - Whether to enable log color support.
39
+ * @property {Console} [logger=console] - A custom logger object (must implement console methods).
37
40
  */
38
41
 
39
42
  /**
@@ -46,7 +49,7 @@ const checkDestroy = utils.createCheckDestroyed('TinyMediaPlayer');
46
49
  /**
47
50
  * A universal media player manager capable of orchestrating multiple API adapters.
48
51
  */
49
- class TinyMediaPlayer extends events.EventEmitter {
52
+ class TinyMediaPlayer extends TinyDebugger {
50
53
  static BaseMediaAdapter = index.BaseMediaAdapter;
51
54
 
52
55
  /**
@@ -266,11 +269,18 @@ class TinyMediaPlayer extends events.EventEmitter {
266
269
  * @throws {RangeError} If numeric options are out of valid ranges.
267
270
  */
268
271
  constructor(options = {}) {
269
- super();
270
272
  if (!objChecker.isValidObj(options)) {
271
273
  throw new TypeError('Options must be a non-null object.');
272
274
  }
273
275
 
276
+ // Debug Configuration
277
+ super({
278
+ id: '[_blue_TinyMediaPlayer_reset_] :debug:',
279
+ logger: options.logger ?? console,
280
+ debugMode: options.debugMode ?? false,
281
+ useLogColors: options.useLogColors ?? false,
282
+ });
283
+
274
284
  // Volume configuration
275
285
  if (options.persistVolume !== undefined) {
276
286
  if (typeof options.persistVolume !== 'boolean') {
@@ -318,6 +328,8 @@ class TinyMediaPlayer extends events.EventEmitter {
318
328
  }
319
329
 
320
330
  if (this.#persistVolume) this.#loadVolumeFromStorage();
331
+
332
+ this.log('info', 'Instance initialized.', { options });
321
333
  }
322
334
 
323
335
  // ==========================================
@@ -335,6 +347,7 @@ class TinyMediaPlayer extends events.EventEmitter {
335
347
  const parsedVolume = parseFloat(storedValue);
336
348
  if (!isNaN(parsedVolume) && parsedVolume >= 0 && parsedVolume <= 1) {
337
349
  this.#volume = parsedVolume;
350
+ this.log('info', `Volume loaded from storage: ${this.#volume}`);
338
351
  }
339
352
  }
340
353
  }
@@ -350,6 +363,7 @@ class TinyMediaPlayer extends events.EventEmitter {
350
363
  try {
351
364
  if (this.#persistVolume && typeof window !== 'undefined' && window.localStorage) {
352
365
  window.localStorage.setItem(this.#volumeStorageKey, this.#volume.toString());
366
+ this.log('info', `Volume saved to storage: ${this.#volume}`);
353
367
  }
354
368
  } catch (error) {
355
369
  console.warn('[TinyMediaPlayer] Failed to save volume to localStorage.', error);
@@ -421,10 +435,15 @@ class TinyMediaPlayer extends events.EventEmitter {
421
435
  const startTime = performance.now();
422
436
  const duration = this.#fadeVolumeSpeed;
423
437
 
438
+ this.log('info', `Starting volume fade: ${startVolume} -> ${targetVolume}`);
439
+
424
440
  return new Promise((resolve) => {
425
441
  /** @param {number} currentTime */
426
442
  const step = (currentTime) => {
427
- if (signal.aborted) return resolve();
443
+ if (signal.aborted) {
444
+ this.log('info', 'Volume fade aborted.');
445
+ return resolve();
446
+ }
428
447
 
429
448
  const elapsed = currentTime - startTime;
430
449
  const progress = Math.min(elapsed / duration, 1);
@@ -436,6 +455,7 @@ class TinyMediaPlayer extends events.EventEmitter {
436
455
  requestAnimationFrame(step);
437
456
  } else {
438
457
  adapter.setVolume(targetVolume);
458
+ this.log('info', `Volume fade completed: ${targetVolume}`);
439
459
  resolve();
440
460
  }
441
461
  };
@@ -477,6 +497,7 @@ class TinyMediaPlayer extends events.EventEmitter {
477
497
  }
478
498
 
479
499
  this.#playlist = value;
500
+ this.log('info', `Playlist updated. New size: ${this.#playlist.length}`);
480
501
  this.emit('playlistUpdate', this.playlist);
481
502
 
482
503
  // Validation to correct currentIndex ensuring it aligns with the new playlist limits
@@ -497,6 +518,7 @@ class TinyMediaPlayer extends events.EventEmitter {
497
518
  if (oldAdapter) await oldAdapter.stop();
498
519
  if (this.#currentIndex !== -1) await this.play();
499
520
  } catch (error) {
521
+ this.log('error', 'Background transition error on playlist update:', error);
500
522
  console.warn('[TinyMediaPlayer] Background transition error on playlist update:', error);
501
523
  }
502
524
  })();
@@ -525,6 +547,8 @@ class TinyMediaPlayer extends events.EventEmitter {
525
547
 
526
548
  if (this.#currentIndex === value) return; // Optimization: do nothing if index is the same
527
549
 
550
+ this.log('info', `Index change requested: ${this.#currentIndex} -> ${value}`);
551
+
528
552
  const wasPlaying = this.#isPlaying;
529
553
  let oldAdapter = null;
530
554
 
@@ -546,6 +570,7 @@ class TinyMediaPlayer extends events.EventEmitter {
546
570
  if (oldAdapter) await oldAdapter.stop();
547
571
  if (this.#currentIndex !== -1) await this.play();
548
572
  } catch (error) {
573
+ this.log('error', 'Background transition error on index update:', error);
549
574
  console.warn('[TinyMediaPlayer] Background transition error on index update:', error);
550
575
  }
551
576
  })();
@@ -569,6 +594,7 @@ class TinyMediaPlayer extends events.EventEmitter {
569
594
  throw new TypeError(`Loop mode must be one of: ${validModes.join(', ')}.`);
570
595
  }
571
596
  this.#loopMode = value;
597
+ this.log('info', `Loop mode changed to: ${this.#loopMode}`);
572
598
  this.emit('loopModeChange', this.#loopMode);
573
599
  }
574
600
 
@@ -588,6 +614,7 @@ class TinyMediaPlayer extends events.EventEmitter {
588
614
  throw new TypeError('Random mode state must be a boolean.');
589
615
  }
590
616
  this.#isRandom = value;
617
+ this.log('info', `Random mode: ${this.#isRandom}`);
591
618
  this.emit('randomModeChange', this.#isRandom);
592
619
  }
593
620
 
@@ -617,6 +644,7 @@ class TinyMediaPlayer extends events.EventEmitter {
617
644
  // Abort any ongoing fade to ensure manual control is immediate
618
645
  if (this.#fadeController) this.#fadeController.abort();
619
646
 
647
+ this.log('info', `Volume changed: ${this.#volume} -> ${value}`);
620
648
  this.#volume = value;
621
649
  this.#saveVolumeToStorage();
622
650
  this.emit('volumeChange', this.#volume);
@@ -648,6 +676,7 @@ class TinyMediaPlayer extends events.EventEmitter {
648
676
  throw new TypeError('Persist volume parameter must be a boolean.');
649
677
  }
650
678
  this.#persistVolume = value;
679
+ this.log('info', `Persist volume: ${this.#persistVolume}`);
651
680
  if (value) {
652
681
  this.#saveVolumeToStorage();
653
682
  }
@@ -693,30 +722,31 @@ class TinyMediaPlayer extends events.EventEmitter {
693
722
  }
694
723
 
695
724
  /**
696
- * Sets the mute state of the player.
697
- * This state is synchronized with the active adapter and respected when playing new tracks.
698
- * @param {boolean} value - True to mute, false to unMute.
699
- * @throws {TypeError} If the value is not a boolean.
725
+ * Mutes the current playback.
726
+ * @returns {Promise<void>}
700
727
  */
701
- set isMuted(value) {
728
+ async mute() {
702
729
  checkDestroy(this.#destroyed);
703
- if (typeof value !== 'boolean') {
704
- throw new TypeError('isMuted must be a boolean.');
705
- }
706
-
707
- this.#isMuted = value;
708
-
709
- // Synchronize with the active adapter immediately
710
- try {
711
- const adapter = this.#getActiveAdapter();
712
- if (adapter) {
713
- value ? adapter.mute() : adapter.unMute();
714
- }
715
- } catch (error) {
716
- // If no active adapter, we just wait for the next play() call to apply the state
717
- }
730
+ const adapter = this.#getActiveAdapter();
731
+ if (!adapter) return;
732
+ await adapter.mute();
733
+ this.#isMuted = true;
734
+ this.emit('muteChange', this.#isMuted);
735
+ this.log('info', 'Playback muted successfully.');
736
+ }
718
737
 
738
+ /**
739
+ * Unmutes the current playback.
740
+ * @returns {Promise<void>}
741
+ */
742
+ async unMute() {
743
+ checkDestroy(this.#destroyed);
744
+ const adapter = this.#getActiveAdapter();
745
+ if (!adapter) return;
746
+ await adapter.unMute();
747
+ this.#isMuted = false;
719
748
  this.emit('muteChange', this.#isMuted);
749
+ this.log('info', 'Playback unmuted successfully.');
720
750
  }
721
751
 
722
752
  // ==========================================
@@ -773,6 +803,8 @@ class TinyMediaPlayer extends events.EventEmitter {
773
803
  throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
774
804
  }
775
805
 
806
+ this.log('info', 'Registering new adapter:', adapter);
807
+
776
808
  const events = new Map();
777
809
  this.#adapterHandlers.set(adapter, events);
778
810
 
@@ -795,6 +827,7 @@ class TinyMediaPlayer extends events.EventEmitter {
795
827
  if (!(adapter instanceof index.BaseMediaAdapter)) {
796
828
  throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
797
829
  }
830
+ this.log('info', 'Removing adapter:', adapter);
798
831
  this.#removeHandler(adapter);
799
832
  return this.#adapters.delete(adapter);
800
833
  }
@@ -809,6 +842,7 @@ class TinyMediaPlayer extends events.EventEmitter {
809
842
  if (!(adapter instanceof index.BaseMediaAdapter)) {
810
843
  throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
811
844
  }
845
+ this.log('info', 'Destroying adapter:', adapter);
812
846
  this.#removeHandler(adapter);
813
847
  const result = this.#adapters.delete(adapter);
814
848
  if (result) adapter.destroy();
@@ -833,6 +867,7 @@ class TinyMediaPlayer extends events.EventEmitter {
833
867
  */
834
868
  destroyAllAdapters() {
835
869
  checkDestroy(this.#destroyed);
870
+ this.log('info', 'Destroying all adapters.');
836
871
  this.#adapters.forEach((adapter) => {
837
872
  this.#removeHandler(adapter);
838
873
  adapter.destroy();
@@ -845,6 +880,7 @@ class TinyMediaPlayer extends events.EventEmitter {
845
880
  */
846
881
  clearAdapters() {
847
882
  checkDestroy(this.#destroyed);
883
+ this.log('info', 'Clearing all adapters.');
848
884
  this.#adapters.forEach((adapter) => this.#removeHandler(adapter));
849
885
  this.#adapters.clear();
850
886
  }
@@ -877,7 +913,9 @@ class TinyMediaPlayer extends events.EventEmitter {
877
913
  const currentContent = this.#playlist[this.#currentIndex];
878
914
  const adapter = this.getMediaAdapter(currentContent);
879
915
  if (adapter) return adapter;
880
- throw new Error(`No compatible adapter found for content ID: ${currentContent.id}.`);
916
+ const errorMsg = `No compatible adapter found for content ID: ${currentContent.id}.`;
917
+ this.log('error', errorMsg);
918
+ throw new Error(errorMsg);
881
919
  }
882
920
 
883
921
  // ==========================================
@@ -898,6 +936,7 @@ class TinyMediaPlayer extends events.EventEmitter {
898
936
  // If the event is already in the list, do nothing to avoid duplicates
899
937
  if (this.#adapterEventNames.has(eventName)) return;
900
938
 
939
+ this.log('info', `Adding adapter event listener: ${eventName}`);
901
940
  this.#adapterEventNames.add(eventName);
902
941
 
903
942
  // Synchronize: Add the listener to all already registered adapters
@@ -919,6 +958,7 @@ class TinyMediaPlayer extends events.EventEmitter {
919
958
 
920
959
  if (!this.#adapterEventNames.has(eventName)) return;
921
960
 
961
+ this.log('info', `Removing adapter event listener: ${eventName}`);
922
962
  this.#adapterEventNames.delete(eventName);
923
963
 
924
964
  // Synchronize: Remove the listener from all registered adapters
@@ -966,6 +1006,8 @@ class TinyMediaPlayer extends events.EventEmitter {
966
1006
  /** @type {(...args: any[]) => any} */
967
1007
  const handler = (...args) => {
968
1008
  if (!this.#destroyed) {
1009
+ if (eventName !== 'timeupdate')
1010
+ this.log('debug', `Adapter event triggered: ${eventName}`, ...args);
969
1011
  this.emit(eventName, ...args);
970
1012
  }
971
1013
  };
@@ -1011,6 +1053,8 @@ class TinyMediaPlayer extends events.EventEmitter {
1011
1053
  mediaContent.valMediaContentMetadata(newContent);
1012
1054
 
1013
1055
  const newLength = this.#playlist.push(newContent);
1056
+ this.log('info', `Track added. New length: ${newLength}`, newContent);
1057
+
1014
1058
  if (this.#currentIndex === -1) {
1015
1059
  this.#currentIndex = 0;
1016
1060
  this.emit('trackChange', this.#currentIndex);
@@ -1067,6 +1111,8 @@ class TinyMediaPlayer extends events.EventEmitter {
1067
1111
  throw new RangeError(`Index ${index} is out of bounds for the current playlist.`);
1068
1112
  }
1069
1113
 
1114
+ this.log('info', `Removing track at index: ${index}`);
1115
+
1070
1116
  if (index === this.#currentIndex) {
1071
1117
  // Stop the current track if it is the one being removed
1072
1118
  await this.stop();
@@ -1103,6 +1149,8 @@ class TinyMediaPlayer extends events.EventEmitter {
1103
1149
  throw new TypeError('Search query must be a string or a boolean evaluation function.');
1104
1150
  }
1105
1151
 
1152
+ this.log('info', 'Searching playlist...', { query });
1153
+
1106
1154
  const results = [];
1107
1155
 
1108
1156
  for (let i = 0; i < this.#playlist.length; i++) {
@@ -1125,6 +1173,7 @@ class TinyMediaPlayer extends events.EventEmitter {
1125
1173
  }
1126
1174
  }
1127
1175
 
1176
+ this.log('info', `Search completed. Found ${results.length} results.`);
1128
1177
  return results;
1129
1178
  }
1130
1179
 
@@ -1133,6 +1182,7 @@ class TinyMediaPlayer extends events.EventEmitter {
1133
1182
  */
1134
1183
  async clearPlaylist() {
1135
1184
  checkDestroy(this.#destroyed);
1185
+ this.log('info', 'Clearing entire playlist.');
1136
1186
  await this.stop();
1137
1187
  this.#playlist = [];
1138
1188
  this.#currentIndex = -1;
@@ -1150,6 +1200,7 @@ class TinyMediaPlayer extends events.EventEmitter {
1150
1200
  */
1151
1201
  async play() {
1152
1202
  checkDestroy(this.#destroyed);
1203
+ this.log('info', `Attempting to play track at index: ${this.#currentIndex}`);
1153
1204
  const adapter = this.#getActiveAdapter();
1154
1205
  if (!adapter) return;
1155
1206
 
@@ -1160,10 +1211,16 @@ class TinyMediaPlayer extends events.EventEmitter {
1160
1211
  adapter.setVolume(this.#volume);
1161
1212
  }
1162
1213
 
1163
- await adapter.play(this.#playlist[this.#currentIndex]);
1164
- if (this.#isMuted) await adapter.mute();
1165
- this.#isPlaying = true;
1166
- this.emit('play', this.#currentIndex);
1214
+ try {
1215
+ await adapter.play(this.#playlist[this.#currentIndex]);
1216
+ if (this.#isMuted) await adapter.mute();
1217
+ this.#isPlaying = true;
1218
+ this.log('info', 'Playback started successfully.');
1219
+ this.emit('play', this.#currentIndex);
1220
+ } catch (error) {
1221
+ this.log('error', 'Failed to play track:', error);
1222
+ throw error;
1223
+ }
1167
1224
  }
1168
1225
 
1169
1226
  /**
@@ -1172,6 +1229,7 @@ class TinyMediaPlayer extends events.EventEmitter {
1172
1229
  */
1173
1230
  async pause() {
1174
1231
  checkDestroy(this.#destroyed);
1232
+ this.log('info', 'Attempting to pause playback.');
1175
1233
  const adapter = this.#getActiveAdapter();
1176
1234
  if (!adapter) return;
1177
1235
 
@@ -1181,6 +1239,7 @@ class TinyMediaPlayer extends events.EventEmitter {
1181
1239
 
1182
1240
  await adapter.pause();
1183
1241
  this.#isPlaying = false;
1242
+ this.log('info', 'Playback paused.');
1184
1243
  this.emit('pause', this.#currentIndex);
1185
1244
  }
1186
1245
 
@@ -1190,6 +1249,7 @@ class TinyMediaPlayer extends events.EventEmitter {
1190
1249
  */
1191
1250
  async stop() {
1192
1251
  checkDestroy(this.#destroyed);
1252
+ this.log('info', 'Attempting to stop playback.');
1193
1253
  const adapter = this.#getActiveAdapter();
1194
1254
  if (!adapter) return;
1195
1255
 
@@ -1199,6 +1259,7 @@ class TinyMediaPlayer extends events.EventEmitter {
1199
1259
 
1200
1260
  await adapter.stop();
1201
1261
  this.#isPlaying = false;
1262
+ this.log('info', 'Playback stopped.');
1202
1263
  this.emit('stop', this.#currentIndex);
1203
1264
  }
1204
1265
 
@@ -1210,9 +1271,11 @@ class TinyMediaPlayer extends events.EventEmitter {
1210
1271
  checkDestroy(this.#destroyed);
1211
1272
  if (this.#playlist.length === 0) return;
1212
1273
 
1274
+ this.log('info', 'Next track requested.');
1213
1275
  await this.stop();
1214
1276
 
1215
1277
  if (this.#loopMode === 'TRACK') {
1278
+ this.log('info', 'Loop mode: TRACK. Repeating current track.');
1216
1279
  // Index remains the same
1217
1280
  await this.play();
1218
1281
  return;
@@ -1220,13 +1283,15 @@ class TinyMediaPlayer extends events.EventEmitter {
1220
1283
 
1221
1284
  if (this.#isRandom) {
1222
1285
  this.#currentIndex = this._getWeightedRandomIndex();
1286
+ this.log('info', `Random mode: Selected index ${this.#currentIndex}`);
1223
1287
  } else {
1224
1288
  let nextIndex = this.#currentIndex + 1;
1225
1289
  if (nextIndex >= this.#playlist.length) {
1226
1290
  if (this.#loopMode === 'PLAYLIST') {
1227
1291
  nextIndex = 0;
1292
+ this.log('info', 'Loop mode: PLAYLIST. Returning to start.');
1228
1293
  } else {
1229
- // End of playlist, stop playing
1294
+ this.log('info', 'End of playlist reached. Stopping.');
1230
1295
  return;
1231
1296
  }
1232
1297
  }
@@ -1247,6 +1312,7 @@ class TinyMediaPlayer extends events.EventEmitter {
1247
1312
 
1248
1313
  // UX: Repeat current track if "repeat on prev" is enabled and it's the first click
1249
1314
  if (this.#repeatCurrentOnPrev && !this.#prevClickedToRepeat) {
1315
+ this.log('info', 'UX: Repeat current track on first "prev" click.');
1250
1316
  this.#prevClickedToRepeat = true;
1251
1317
 
1252
1318
  // Reset the "repeat" state after 2 seconds of inactivity to prevent glitches
@@ -1268,17 +1334,20 @@ class TinyMediaPlayer extends events.EventEmitter {
1268
1334
  }
1269
1335
  this.#prevClickedToRepeat = false;
1270
1336
 
1337
+ this.log('info', 'Previous track requested.');
1271
1338
  await this.stop();
1272
1339
 
1273
1340
  if (this.#isRandom) {
1274
1341
  this.#currentIndex = this._getWeightedRandomIndex();
1342
+ this.log('info', `Random mode: Selected index ${this.#currentIndex}`);
1275
1343
  } else {
1276
1344
  let prevIndex = this.#currentIndex - 1;
1277
1345
  if (prevIndex < 0) {
1278
1346
  if (this.#loopMode === 'PLAYLIST') {
1279
1347
  prevIndex = this.#playlist.length - 1;
1348
+ this.log('info', 'Loop mode: PLAYLIST. Returning to end.');
1280
1349
  } else {
1281
- // End of playlist, stop playing
1350
+ this.log('info', 'Start of playlist reached. Stopping.');
1282
1351
  return;
1283
1352
  }
1284
1353
  }
@@ -1289,6 +1358,29 @@ class TinyMediaPlayer extends events.EventEmitter {
1289
1358
  await this.play();
1290
1359
  }
1291
1360
 
1361
+ /**
1362
+ * Jumps directly to a specific index in the playlist.
1363
+ * @param {number} index - The target index to skip to.
1364
+ * @returns {Promise<void>}
1365
+ * @throws {TypeError} If the index is not a number.
1366
+ * @throws {RangeError} If the index is out of the playlist bounds.
1367
+ */
1368
+ async skipTo(index) {
1369
+ checkDestroy(this.#destroyed);
1370
+ if (typeof index !== 'number') {
1371
+ throw new TypeError('Index must be a number.');
1372
+ }
1373
+ if (!this.existsTrack(index)) {
1374
+ throw new RangeError(`Index ${index} is out of bounds for the current playlist.`);
1375
+ }
1376
+
1377
+ this.log('info', `Skipping to index: ${index}`);
1378
+ await this.stop();
1379
+ this.#currentIndex = index;
1380
+ this.emit('trackChange', this.#currentIndex);
1381
+ await this.play();
1382
+ }
1383
+
1292
1384
  /**
1293
1385
  * Jumps to a specific absolute time in the timeline.
1294
1386
  * @param {number} timeMs - The target time in milliseconds.
@@ -1305,6 +1397,7 @@ class TinyMediaPlayer extends events.EventEmitter {
1305
1397
  throw new RangeError('Seek time cannot be negative.');
1306
1398
  }
1307
1399
 
1400
+ this.log('info', `Seeking to: ${timeMs}ms`);
1308
1401
  const adapter = this.#getActiveAdapter();
1309
1402
  if (adapter) {
1310
1403
  await adapter.seek(timeMs);
@@ -1324,6 +1417,7 @@ class TinyMediaPlayer extends events.EventEmitter {
1324
1417
  throw new TypeError('Step amount must be a number in milliseconds.');
1325
1418
  }
1326
1419
 
1420
+ this.log('info', `Stepping by: ${stepMs}ms`);
1327
1421
  const adapter = this.#getActiveAdapter();
1328
1422
  if (!adapter) return;
1329
1423
 
@@ -1396,6 +1490,34 @@ class TinyMediaPlayer extends events.EventEmitter {
1396
1490
  }
1397
1491
  }
1398
1492
 
1493
+ /**
1494
+ * Checks whether the media adapter is paused.
1495
+ * @returns {boolean} True if the adapter is ready, false otherwise.
1496
+ */
1497
+ get isPaused() {
1498
+ checkDestroy(this.#destroyed);
1499
+ try {
1500
+ const adapter = this.#getActiveAdapter();
1501
+ return adapter ? adapter.isPaused() : false;
1502
+ } catch {
1503
+ return false;
1504
+ }
1505
+ }
1506
+
1507
+ /**
1508
+ * Checks whether the media adapter is ended.
1509
+ * @returns {boolean} True if the adapter is ready, false otherwise.
1510
+ */
1511
+ get isEnded() {
1512
+ checkDestroy(this.#destroyed);
1513
+ try {
1514
+ const adapter = this.#getActiveAdapter();
1515
+ return adapter ? adapter.isEnded() : true;
1516
+ } catch {
1517
+ return true;
1518
+ }
1519
+ }
1520
+
1399
1521
  /**
1400
1522
  * Retrieves the metadata of the currently loaded content, returning a structured
1401
1523
  * object containing details such as the content ID, title, author, and duration.
@@ -1448,12 +1570,15 @@ class TinyMediaPlayer extends events.EventEmitter {
1448
1570
  async destroy() {
1449
1571
  if (this.#destroyed) return;
1450
1572
 
1573
+ this.log('info', 'Destroying TinyMediaPlayer instance.');
1574
+
1451
1575
  // 1. Stop current playback if active to halt media processes
1452
1576
  try {
1453
1577
  if (this.#isPlaying) {
1454
1578
  await this.stop();
1455
1579
  }
1456
1580
  } catch (error) {
1581
+ this.log('error', 'Non-fatal error during playback termination:', error);
1457
1582
  console.warn('[TinyMediaPlayer] Non-fatal error during playback termination:', error);
1458
1583
  }
1459
1584
 
@@ -1,4 +1,4 @@
1
- import { EventEmitter } from 'events';
1
+ import TinyDebugger from './TinyDebugger.mjs';
2
2
  import { BaseMediaAdapter } from './TinyMediaPlayer/index.mjs';
3
3
  export type PictureDataType = import('../basics/mediaContent.mjs').PictureDataType;
4
4
  export type MediaContent = import('../basics/mediaContent.mjs').MediaContent<PictureDataType>;
@@ -32,6 +32,18 @@ export type TinyMediaPlayerOptions = {
32
32
  * - If true, volume fades smoothly to zero when stopping.
33
33
  */
34
34
  smoothStopVolume?: boolean;
35
+ /**
36
+ * - Whether to enable internal debug logging.
37
+ */
38
+ debugMode?: boolean;
39
+ /**
40
+ * - Whether to enable log color support.
41
+ */
42
+ useLogColors?: boolean;
43
+ /**
44
+ * - A custom logger object (must implement console methods).
45
+ */
46
+ logger?: Console;
35
47
  };
36
48
  export type SearchResult = {
37
49
  /**
@@ -67,6 +79,9 @@ export type SearchResult = {
67
79
  * @property {boolean} [repeatCurrentOnPrev=false] - If true, clicking 'previous' repeats the current track on the first click.
68
80
  * @property {boolean} [smoothPlayPauseVolume=false] - If true, volume fades smoothly during play/pause transitions.
69
81
  * @property {boolean} [smoothStopVolume=false] - If true, volume fades smoothly to zero when stopping.
82
+ * @property {boolean} [debugMode=false] - Whether to enable internal debug logging.
83
+ * @property {boolean} [useLogColors=false] - Whether to enable log color support.
84
+ * @property {Console} [logger=console] - A custom logger object (must implement console methods).
70
85
  */
71
86
  /**
72
87
  * Represents a search match containing the media object and its exact playlist index.
@@ -77,7 +92,7 @@ export type SearchResult = {
77
92
  /**
78
93
  * A universal media player manager capable of orchestrating multiple API adapters.
79
94
  */
80
- declare class TinyMediaPlayer extends EventEmitter {
95
+ declare class TinyMediaPlayer extends TinyDebugger {
81
96
  #private;
82
97
  static BaseMediaAdapter: typeof BaseMediaAdapter;
83
98
  /**
@@ -223,12 +238,15 @@ declare class TinyMediaPlayer extends EventEmitter {
223
238
  /** @returns {boolean} True if the player is currently muted. */
224
239
  get isMuted(): boolean;
225
240
  /**
226
- * Sets the mute state of the player.
227
- * This state is synchronized with the active adapter and respected when playing new tracks.
228
- * @param {boolean} value - True to mute, false to unMute.
229
- * @throws {TypeError} If the value is not a boolean.
241
+ * Mutes the current playback.
242
+ * @returns {Promise<void>}
243
+ */
244
+ mute(): Promise<void>;
245
+ /**
246
+ * Unmutes the current playback.
247
+ * @returns {Promise<void>}
230
248
  */
231
- set isMuted(value: boolean);
249
+ unMute(): Promise<void>;
232
250
  get adapters(): BaseMediaAdapter[];
233
251
  get adaptersSize(): number;
234
252
  static get defaultAdapterEventNames(): string[];
@@ -356,6 +374,14 @@ declare class TinyMediaPlayer extends EventEmitter {
356
374
  * @returns {Promise<void>}
357
375
  */
358
376
  prev(): Promise<void>;
377
+ /**
378
+ * Jumps directly to a specific index in the playlist.
379
+ * @param {number} index - The target index to skip to.
380
+ * @returns {Promise<void>}
381
+ * @throws {TypeError} If the index is not a number.
382
+ * @throws {RangeError} If the index is out of the playlist bounds.
383
+ */
384
+ skipTo(index: number): Promise<void>;
359
385
  /**
360
386
  * Jumps to a specific absolute time in the timeline.
361
387
  * @param {number} timeMs - The target time in milliseconds.
@@ -391,6 +417,16 @@ declare class TinyMediaPlayer extends EventEmitter {
391
417
  * @returns {number} A value between 0 and 100.
392
418
  */
393
419
  getPlaybackPercentage(): number;
420
+ /**
421
+ * Checks whether the media adapter is paused.
422
+ * @returns {boolean} True if the adapter is ready, false otherwise.
423
+ */
424
+ get isPaused(): boolean;
425
+ /**
426
+ * Checks whether the media adapter is ended.
427
+ * @returns {boolean} True if the adapter is ready, false otherwise.
428
+ */
429
+ get isEnded(): boolean;
394
430
  /**
395
431
  * Retrieves the metadata of the currently loaded content, returning a structured
396
432
  * object containing details such as the content ID, title, author, and duration.