tiny-essentials 1.28.17 โ†’ 1.28.18

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.
@@ -2,6 +2,7 @@ import { EventEmitter } from 'events';
2
2
  import { blobUrlToBase64, convertToBlobUrl, parseMediaMetadata, revokeContentUrls, valMediaContentMetadata, } from '../../basics/mediaContent.mjs';
3
3
  import { createCheckDestroyed } from '../utils/tools.mjs';
4
4
  import { isJsonObject } from '../../basics/objChecker.mjs';
5
+ import TinyAdvancedRaffle from '../math/TinyAdvancedRaffle.mjs';
5
6
  const checkDestroy = createCheckDestroyed('TinyRadioFm');
6
7
  /**
7
8
  * @typedef {import('../../basics/mediaContent.mjs').PictureDataType} PictureDataType
@@ -71,12 +72,13 @@ const checkDestroy = createCheckDestroyed('TinyRadioFm');
71
72
  * @property {number} silenceDuration - Gap in ms between tracks.
72
73
  * @property {number} queryLimit - Safety lock for max items processed.
73
74
  * @property {boolean} voiceAfterMusic - Whether to play voice messages after music tracks.
75
+ * @property {number} voiceWeight - Weight used to calculate the probability of a voice block occurring.
74
76
  * @property {number} voiceMin - Minimum amount of voice messages to play if voiceAfterMusic is true.
75
77
  * @property {number} voiceMax - Maximum amount of voice messages to play.
76
78
  * @property {number} musicMaxConsecutive - Max times a music track can repeat consecutively (-1 = unlimited, 0 = strictly no repetition).
77
79
  * @property {number} musicMinConsecutive - Min times a music track must repeat consecutively (1 = no mandatory repetition).
78
80
  * @property {number} voiceMaxConsecutive - Max times a voice track can repeat consecutively (-1 = unlimited, 0 = strictly no repetition).
79
- * @property {number} voiceMinConsecutive - Min times a voice track must repeat consecutively (1 = no mandatory repetition).
81
+ * @property {number} voiceMinConsecutive - Min times a voice track can repeat consecutively (1 = no mandatory repetition).
80
82
  */
81
83
  //////////////////////////////////////////////////////////////////
82
84
  /**
@@ -302,6 +304,7 @@ class TinyRadioFm extends EventEmitter {
302
304
  voiceAfterMusic: true,
303
305
  voiceMin: 0,
304
306
  voiceMax: 1,
307
+ voiceWeight: 1,
305
308
  musicMaxConsecutive: 0,
306
309
  musicMinConsecutive: 0,
307
310
  voiceMaxConsecutive: 0,
@@ -363,6 +366,7 @@ class TinyRadioFm extends EventEmitter {
363
366
  throw new TypeError(`${minKey} must be a non-negative number.`);
364
367
  }
365
368
  };
369
+ checkRange(config.voiceWeight, 'voiceWeight');
366
370
  checkRange(config.voiceMin, 'voiceMin');
367
371
  checkRange(config.voiceMax, 'voiceMax');
368
372
  checkRange(config.musicMinConsecutive, 'musicMinConsecutive');
@@ -838,11 +842,6 @@ class TinyRadioFm extends EventEmitter {
838
842
  consecutiveCount: initialLastId === lastItem.id ? initialConsecutiveCount + 1 : 1,
839
843
  };
840
844
  }
841
- /**
842
- * Pool of available items for weighted selection.
843
- * @type {Array<MediaContent & { weight: number }>}
844
- */
845
- const pool = list.map((item) => ({ ...item, weight: item.weight ?? 1 }));
846
845
  const random = this._prng(currentSeed);
847
846
  /**
848
847
  * The finalized deterministic sequence.
@@ -852,6 +851,27 @@ class TinyRadioFm extends EventEmitter {
852
851
  /** @type {string|null} */
853
852
  let lastId = initialLastId;
854
853
  let consecutiveCount = initialConsecutiveCount;
854
+ // Initializes TinyAdvancedRaffle specifically for this sequence generation
855
+ const raffle = new TinyAdvancedRaffle({
856
+ rng: random,
857
+ normalization: 'relative',
858
+ });
859
+ // We keep the pool as an array to respect the original structure and support possible
860
+ // duplicate IDs, if they occur (even though it is rare).
861
+ /** @type {MediaContent[]} */
862
+ const pool = [...list];
863
+ // Registers all items in the Raffle system
864
+ pool.forEach((item) => {
865
+ const itemWeight = item.weight ?? 1;
866
+ const existing = raffle.getItem(item.id);
867
+ if (!existing) {
868
+ raffle.addItem(item.id, { weight: itemWeight });
869
+ }
870
+ else {
871
+ // If the ID already exists, we add its weight to maintain the correct proportion
872
+ raffle.setBaseWeight(item.id, existing.baseWeight + itemWeight);
873
+ }
874
+ });
855
875
  while (pool.length > 0) {
856
876
  /** @type {MediaContent|null} */
857
877
  let selectedItem = null;
@@ -867,32 +887,28 @@ class TinyRadioFm extends EventEmitter {
867
887
  selectedItem = sequence[sequence.length - 1];
868
888
  }
869
889
  }
870
- // 2. Maximum repetition logic and random draw
890
+ // 2. Maximum repetition logic and drawing using the Raffle
871
891
  if (!selectedItem) {
872
- let validPool = pool;
892
+ let excludedId = null;
873
893
  if (lastId !== null && maxConsecutive !== -1 && consecutiveCount >= maxConsecutive) {
874
- validPool = pool.filter((item) => item.id !== lastId);
875
- }
876
- if (validPool.length === 0) {
877
- validPool = pool;
878
- }
879
- const totalWeight = validPool.reduce((sum, item) => sum + item.weight, 0);
880
- if (totalWeight <= 0) {
881
- selectedItem = validPool[0];
882
- selectedIndex = pool.findIndex((p) => p.id === selectedItem?.id);
894
+ // Checks if there are other different items in the pool before preventing the last one
895
+ const hasOtherItems = pool.some((item) => item.id !== lastId);
896
+ if (hasOtherItems) {
897
+ raffle.excludeItem(lastId);
898
+ excludedId = lastId;
899
+ }
883
900
  }
884
- else {
885
- const r = random() * totalWeight;
886
- let sum = 0;
887
- for (let i = 0; i < validPool.length; i++) {
888
- sum += validPool[i].weight;
889
- if (r <= sum) {
890
- selectedItem = validPool[i];
891
- selectedIndex = pool.findIndex((p) => p.id === selectedItem?.id);
892
- break;
893
- }
901
+ const drawResult = raffle.drawOne();
902
+ if (drawResult) {
903
+ selectedIndex = pool.findIndex((item) => item.id === drawResult.id);
904
+ if (selectedIndex !== -1) {
905
+ selectedItem = pool[selectedIndex];
894
906
  }
895
907
  }
908
+ // Removes the exclusion to avoid breaking future draws
909
+ if (excludedId) {
910
+ raffle.includeItem(excludedId);
911
+ }
896
912
  }
897
913
  // Safety fallback
898
914
  if (!selectedItem) {
@@ -900,8 +916,25 @@ class TinyRadioFm extends EventEmitter {
900
916
  selectedIndex = 0;
901
917
  }
902
918
  sequence.push(selectedItem);
919
+ // Updates the pool and the Raffle after the item has been drawn and used
903
920
  if (selectedIndex !== -1) {
904
921
  pool.splice(selectedIndex, 1);
922
+ // If this item is no longer in the pool permanently, remove it from the Raffle
923
+ const stillInPool = pool.some((item) => item.id === selectedItem.id);
924
+ if (!stillInPool) {
925
+ raffle.removeItem(selectedItem.id);
926
+ }
927
+ else {
928
+ // If it still exists, we reduce the weight proportionally to continue the draw fairly
929
+ const existing = raffle.getItem(selectedItem.id);
930
+ if (existing) {
931
+ const newWeight = Math.max(0, existing.baseWeight - (selectedItem.weight ?? 1));
932
+ if (newWeight <= 0)
933
+ raffle.removeItem(selectedItem.id);
934
+ else
935
+ raffle.setBaseWeight(selectedItem.id, newWeight);
936
+ }
937
+ }
905
938
  }
906
939
  if (selectedItem.id === lastId) {
907
940
  consecutiveCount++;
@@ -911,6 +944,7 @@ class TinyRadioFm extends EventEmitter {
911
944
  consecutiveCount = 1;
912
945
  }
913
946
  }
947
+ raffle.destroy(); // Ensures memory is cleared at the end of each cycle
914
948
  return { items: sequence, lastId, consecutiveCount };
915
949
  }
916
950
  /**
@@ -956,17 +990,22 @@ class TinyRadioFm extends EventEmitter {
956
990
  });
957
991
  cycleDuration += music.duration + this.#config.silenceDuration;
958
992
  if (this.#config.voiceAfterMusic && voiceSeq.length > 0) {
959
- const range = this.#config.voiceMax - this.#config.voiceMin + 1;
960
- const voiceAmount = Math.floor(mixRandom() * range) + this.#config.voiceMin;
961
- for (let v = 0; v < voiceAmount; v++) {
962
- const voice = voiceSeq[voiceCursor % voiceSeq.length];
963
- voiceCursor++;
964
- block.push({
965
- ...voice,
966
- cycleStart: cycleDuration,
967
- cycleEnd: cycleDuration + voice.duration,
968
- });
969
- cycleDuration += voice.duration + this.#config.silenceDuration;
993
+ // Calculate chance based on weight: P = W / (W + 1)
994
+ const voiceChance = this.#config.voiceWeight / (this.#config.voiceWeight + 1);
995
+ // Only proceed if the random roll is less than the calculated chance
996
+ if (mixRandom() < voiceChance) {
997
+ const range = this.#config.voiceMax - this.#config.voiceMin + 1;
998
+ const voiceAmount = Math.floor(mixRandom() * range) + this.#config.voiceMin;
999
+ for (let v = 0; v < voiceAmount; v++) {
1000
+ const voice = voiceSeq[voiceCursor % voiceSeq.length];
1001
+ voiceCursor++;
1002
+ block.push({
1003
+ ...voice,
1004
+ cycleStart: cycleDuration,
1005
+ cycleEnd: cycleDuration + voice.duration,
1006
+ });
1007
+ cycleDuration += voice.duration + this.#config.silenceDuration;
1008
+ }
970
1009
  }
971
1010
  }
972
1011
  }
@@ -12,9 +12,9 @@
12
12
  * **๐ŸŽต Dual-Stream Logic:** Manages separate playlists for `music` and `voice`, with configurable interaction rules (e.g., playing voices after music).
13
13
  * **๐Ÿ“… Scheduled Mutations:** Schedule tasks to `add`, `remove`, or `move` items in the playlist at specific future timestamps.
14
14
  * **โšก Custom Injections:** Inject "Custom Positions" into the timeline that interrupt the standard cycle.
15
- * **๐Ÿง  Smart Queuing:** Intelligent logic to prevent audio overlaps by automatically adjusting timestamps to the end of currently playing content.
15
+ * **๐Ÿง  Smart Queuing:** Intelligent logic to prevent audio overlaps by automatically adjusting timestamps to the end of the currently playing content.
16
16
  * **๐Ÿ’พ State Persistence:** Full export/import capabilities, including automatic conversion of Blob URLs to Base64 for easy storage.
17
- * **๐Ÿงน Memory Management:** Proactive cleanup of Blob URLs and event listeners to prevent memory leaks.
17
+ * **๐Ÿงน Memory Management:** Proactive cleanup of Blob URLs and event listeners to prevent memory leaks, with configurable optimization for media and pictures.
18
18
 
19
19
  ---
20
20
 
@@ -31,6 +31,7 @@ The behavior of the engine is controlled via the `RadioConfig` object.
31
31
  | `voiceAfterMusic` | `boolean` | `true` | If `true`, voice messages play after music tracks. |
32
32
  | `voiceMin` | `number` | `0` | Minimum number of voice messages to play in a cycle. |
33
33
  | `voiceMax` | `number` | `1` | Maximum number of voice messages to play in a cycle. |
34
+ | `voiceWeight` | `number` | `1` | Weight used to calculate the probability of a voice block occurring ($P = W / (W + 1)$). |
34
35
  | `musicMinConsecutive`| `number` | `0` | Min times a music track must repeat consecutively. |
35
36
  | `musicMaxConsecutive`| `number` | `0` | Max consecutive music repeats (`-1` = unlimited, `0` = no repeats). |
36
37
  | `voiceMinConsecutive`| `number` | `0` | Min times a voice track must repeat consecutively. |
@@ -50,11 +51,41 @@ Prepares a `MediaContent` object by extracting metadata from an audio source.
50
51
  * `metadata`: Optional manual overrides.
51
52
  * `parseFile`: Internal helper for file parsing.
52
53
  * `callbacks`: Object containing `onProgress` and `onError` handlers.
53
- * `convertBase64toBlob`: If true, the image content will be converted directly into a high-performance Blob URL, use this method.
54
+ * `convertBase64toBlob`: If true, the image content will be converted directly into a high-performance Blob URL.
54
55
  * **Returns:** `Promise<MediaContent>`
55
56
  * **Throws:** `MediaLoadingError`
56
57
 
57
- ---
58
+ #### `static get/set unknownArtist(value)`
59
+ Gets or sets the identifier used when an artist cannot be determined.
60
+ * **Value:** A `string` or a `function` that returns a `string`.
61
+ * **Throws:** `TypeError` if the value is not a string or a function.
62
+
63
+ ### ๐Ÿ“‹ Properties (Getters & Setters)
64
+
65
+ #### **System State & Lifecycle**
66
+ * `destroyed`: Returns `true` if the instance has been destroyed.
67
+ * `size`: Total count of all content items (music + voice).
68
+ * `seed`: Gets/Sets the core randomness seed.
69
+ * `anchorDate`: Gets the absolute timestamp used as the timeline anchor.
70
+ * `cycleCacheSize`: Returns the number of items currently stored in the cycle cache.
71
+
72
+ #### **Playlist Access (Returns Deep Clones)**
73
+ * `musicList`: Returns a deep clone of the music playlist.
74
+ * `voiceList`: Returns a deep clone of the voice playlist.
75
+ * `allList`: Returns a deep clone of the combined music and voice playlists.
76
+ * `customPositions`: Returns a deep clone of the custom position injections.
77
+ * `scheduledTasks`: Returns a deep clone of the pending scheduled tasks.
78
+
79
+ #### **Counters**
80
+ * `musicSize`: Total number of items in the music playlist.
81
+ * `voiceSize`: Total number of items in the voice playlist.
82
+ * `customPosSize`: Number of active custom position injections.
83
+ * `tasksSize`: Number of pending scheduled tasks.
84
+
85
+ #### **Configuration & Optimization**
86
+ * `config`: Gets/Sets the current `RadioConfig` object.
87
+ * `optimizeMediaMemory`: Gets/Sets whether Blob URLs for media should be converted to Base64 during export.
88
+ * `optimizePictureMemory`: Gets/Sets whether Blob URLs for pictures should be converted to Base64 during export.
58
89
 
59
90
  ### ๐Ÿ“ฅ Content Management
60
91
 
@@ -67,12 +98,10 @@ Adds new content to the engine immediately.
67
98
  * **Emits:** `contentAdded`
68
99
 
69
100
  #### `remove(id)`
70
- Instantly removes content by its unique ID across all lists, custom positions, and pending tasks. It also revokes associated Blob URLs.
101
+ Instantly removes content by its unique ID across all lists, custom positions, and pending tasks. It also revokes associated Blob URLs to free memory.
71
102
  * **Parameters:** `id` (string)
72
103
  * **Emits:** `contentRemoved`
73
104
 
74
- ---
75
-
76
105
  ### ๐Ÿ“… Task & Config Management
77
106
 
78
107
  #### `scheduleTask(timestamp, action, type, payload, smartQueue = true)`
@@ -90,8 +119,6 @@ Performs a partial or full update of the engine configuration.
90
119
  * **Parameters:** `config` (Partial or full `RadioConfig` object).
91
120
  * **Emits:** `configChanged`
92
121
 
93
- ---
94
-
95
122
  ### ๐Ÿ” Timeline & Querying
96
123
 
97
124
  #### `getCurrentEvent()`
@@ -106,7 +133,9 @@ Predicts upcoming events from a specific date forward using a virtual sandbox to
106
133
  * **Returns:** `RadioEvent[]`
107
134
  * **Throws:** `TypeError` or `RangeError` if limit is invalid.
108
135
 
109
- ---
136
+ #### `searchCustomPositions()`
137
+ Returns a shallow copy of all active custom positions currently injected into the timeline.
138
+ * **Returns:** `CustomPosition[]`
110
139
 
111
140
  ### ๐Ÿ’พ Persistence & Lifecycle
112
141
 
@@ -161,4 +190,4 @@ A structural block representing a single full iteration of the radio sequence.
161
190
  * `lastMusicId`: ID of the last music played in this block.
162
191
  * `musicConsecutiveCount`: Current consecutive count for music.
163
192
  * `lastVoiceId`: ID of the last voice played in this block.
164
- * `voiceConsecutiveCount`: Current consecutive count for voice.
193
+ * `voiceConsecutiveCount`: Current consecutive count for voice.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-essentials",
3
- "version": "1.28.17",
3
+ "version": "1.28.18",
4
4
  "description": "Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.",
5
5
  "bin": {
6
6
  "tiny-essentials-fork": "./TinyFork.mjs"