tiny-essentials 1.28.7 → 1.28.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog/1/28/9.md +17 -0
- package/dist/v1/TinyBasicsEs.min.js +1 -1
- package/dist/v1/TinyDebugger.min.js +1 -0
- package/dist/v1/TinyEssentials.min.js +1 -1
- package/dist/v1/TinyMediaPlayer.min.js +1 -1
- package/dist/v1/TinyRadioFm.min.js +1 -1
- package/dist/v1/TinyUploadClicker.min.js +1 -1
- package/dist/v1/basics/browserDetector.cjs +228 -0
- package/dist/v1/basics/browserDetector.d.mts +66 -0
- package/dist/v1/basics/browserDetector.mjs +207 -0
- package/dist/v1/basics/index.cjs +6 -0
- package/dist/v1/basics/index.d.mts +2 -1
- package/dist/v1/basics/index.mjs +2 -1
- package/dist/v1/build/TinyDebugger.cjs +7 -0
- package/dist/v1/build/TinyDebugger.d.mts +3 -0
- package/dist/v1/build/TinyDebugger.mjs +2 -0
- package/dist/v1/index.cjs +8 -0
- package/dist/v1/index.d.mts +3 -1
- package/dist/v1/index.mjs +4 -2
- package/dist/v1/libs/TinyDebugger.cjs +380 -0
- package/dist/v1/libs/TinyDebugger.d.mts +132 -0
- package/dist/v1/libs/TinyDebugger.mjs +351 -0
- package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.cjs +39 -3
- package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.d.mts +10 -0
- package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.mjs +35 -3
- package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.cjs +39 -3
- package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.d.mts +10 -0
- package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.mjs +35 -3
- package/dist/v1/libs/TinyMediaPlayer/Mock.cjs +333 -0
- package/dist/v1/libs/TinyMediaPlayer/Mock.d.mts +154 -0
- package/dist/v1/libs/TinyMediaPlayer/Mock.mjs +296 -0
- package/dist/v1/libs/TinyMediaPlayer/SoundCloud.cjs +30 -0
- package/dist/v1/libs/TinyMediaPlayer/SoundCloud.d.mts +10 -0
- package/dist/v1/libs/TinyMediaPlayer/SoundCloud.mjs +26 -0
- package/dist/v1/libs/TinyMediaPlayer/Youtube.cjs +39 -3
- package/dist/v1/libs/TinyMediaPlayer/Youtube.d.mts +10 -0
- package/dist/v1/libs/TinyMediaPlayer/Youtube.mjs +32 -3
- package/dist/v1/libs/TinyMediaPlayer/index.cjs +18 -0
- package/dist/v1/libs/TinyMediaPlayer/index.d.mts +10 -0
- package/dist/v1/libs/TinyMediaPlayer/index.mjs +16 -0
- package/dist/v1/libs/TinyMediaPlayer.cjs +156 -31
- package/dist/v1/libs/TinyMediaPlayer.d.mts +43 -7
- package/dist/v1/libs/TinyMediaPlayer.mjs +148 -31
- package/dist/v1/libs/TinyPromiseQueue.cjs +6 -4
- package/dist/v1/libs/TinyPromiseQueue.d.mts +8 -6
- package/dist/v1/libs/TinyPromiseQueue.mjs +6 -4
- package/dist/v1/libs/TinyRadioFm.cjs +271 -138
- package/dist/v1/libs/TinyRadioFm.d.mts +67 -3
- package/dist/v1/libs/TinyRadioFm.mjs +239 -123
- package/docs/v1/README.md +2 -0
- package/docs/v1/basics/browserDetector.md +135 -0
- package/docs/v1/libs/TinyDebugger.md +119 -0
- package/docs/v1/libs/TinyMediaPlayer.md +26 -22
- package/docs/v1/libs/TinyRadioFm.md +25 -3
- package/package.json +13 -1
|
@@ -87,7 +87,9 @@ const checkDestroy = utils.createCheckDestroyed('TinyRadioFm');
|
|
|
87
87
|
* @property {number} voiceMin - Minimum amount of voice messages to play if voiceAfterMusic is true.
|
|
88
88
|
* @property {number} voiceMax - Maximum amount of voice messages to play.
|
|
89
89
|
* @property {number} musicMaxConsecutive - Max times a music track can repeat consecutively (-1 = unlimited, 0 = strictly no repetition).
|
|
90
|
+
* @property {number} musicMinConsecutive - Min times a music track must repeat consecutively (1 = no mandatory repetition).
|
|
90
91
|
* @property {number} voiceMaxConsecutive - Max times a voice track can repeat consecutively (-1 = unlimited, 0 = strictly no repetition).
|
|
92
|
+
* @property {number} voiceMinConsecutive - Min times a voice track must repeat consecutively (1 = no mandatory repetition).
|
|
91
93
|
*/
|
|
92
94
|
|
|
93
95
|
//////////////////////////////////////////////////////////////////
|
|
@@ -102,6 +104,10 @@ const checkDestroy = utils.createCheckDestroyed('TinyRadioFm');
|
|
|
102
104
|
* @typedef {Object} CycleBlock
|
|
103
105
|
* @property {CycleBlockData[]} items - Items belonging to this cycle.
|
|
104
106
|
* @property {number} duration - Total duration of the cycle block in ms.
|
|
107
|
+
* @property {string|null} lastMusicId - ID of the last music played in this block.
|
|
108
|
+
* @property {number} musicConsecutiveCount - Current consecutive count for music.
|
|
109
|
+
* @property {string|null} lastVoiceId - ID of the last voice played in this block.
|
|
110
|
+
* @property {number} voiceConsecutiveCount - Current consecutive count for voice.
|
|
105
111
|
*/
|
|
106
112
|
|
|
107
113
|
/**
|
|
@@ -138,8 +144,8 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
138
144
|
* extracting metadata from an audio source.
|
|
139
145
|
*
|
|
140
146
|
* @param {string | HTMLMediaElement} source - A URL string or an existing Audio object.
|
|
141
|
-
* @param {Partial<MediaContentBase & MediaContentMetadata> & { id?: string; weight?: number }} [defaultMetadata={}] - Optional default metadata
|
|
142
|
-
* @param {Partial<MediaContentBase & MediaContentMetadata> & { id?: string; weight?: number }} [metadata={}] - Optional manual metadata
|
|
147
|
+
* @param {Partial<MediaContentBase & MediaContentMetadata> & { id?: string; weight?: number }} [defaultMetadata={}] - Optional default metadata.
|
|
148
|
+
* @param {Partial<MediaContentBase & MediaContentMetadata> & { id?: string; weight?: number }} [metadata={}] - Optional manual metadata.
|
|
143
149
|
* @param {ParseMediaContentMetadata} [parseFile] - Private helper to interface with parseFile.
|
|
144
150
|
* @param {Object} [callbacks={}] - Callbacks for monitoring the loading process.
|
|
145
151
|
* @param {(progress: LoadingMediaProgress) => void} [callbacks.onProgress] - Callback triggered on stage changes.
|
|
@@ -343,7 +349,9 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
343
349
|
voiceMin: 0,
|
|
344
350
|
voiceMax: 1,
|
|
345
351
|
musicMaxConsecutive: 0,
|
|
352
|
+
musicMinConsecutive: 0,
|
|
346
353
|
voiceMaxConsecutive: 0,
|
|
354
|
+
voiceMinConsecutive: 0,
|
|
347
355
|
};
|
|
348
356
|
/**
|
|
349
357
|
* Gets a deep clone of the current radio configuration.
|
|
@@ -382,7 +390,7 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
382
390
|
|
|
383
391
|
const modes = ['playlist', 'random'];
|
|
384
392
|
|
|
385
|
-
//
|
|
393
|
+
// Type and Bounds Validation
|
|
386
394
|
if (config.mode !== undefined && !modes.includes(config.mode)) {
|
|
387
395
|
throw new TypeError(`Invalid mode: "${config.mode}". Must be one of: ${modes.join(', ')}.`);
|
|
388
396
|
}
|
|
@@ -411,43 +419,83 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
411
419
|
throw new TypeError('voiceAfterMusic must be a boolean.');
|
|
412
420
|
}
|
|
413
421
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
422
|
+
const checkRange = (/** @type {number | undefined} */ val, /** @type {string} */ minKey) => {
|
|
423
|
+
if (val !== undefined && (typeof val !== 'number' || val < 0)) {
|
|
424
|
+
throw new TypeError(`${minKey} must be a non-negative number.`);
|
|
425
|
+
}
|
|
426
|
+
};
|
|
420
427
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
)
|
|
425
|
-
|
|
428
|
+
checkRange(config.voiceMin, 'voiceMin');
|
|
429
|
+
checkRange(config.voiceMax, 'voiceMax');
|
|
430
|
+
checkRange(config.musicMinConsecutive, 'musicMinConsecutive');
|
|
431
|
+
checkRange(config.musicMaxConsecutive, 'musicMaxConsecutive');
|
|
432
|
+
checkRange(config.voiceMinConsecutive, 'voiceMinConsecutive');
|
|
433
|
+
checkRange(config.voiceMaxConsecutive, 'voiceMaxConsecutive');
|
|
434
|
+
|
|
435
|
+
// Logical cross-field validation
|
|
436
|
+
const vMin = config.voiceMin ?? this.#config.voiceMin;
|
|
437
|
+
const vMax = config.voiceMax ?? this.#config.voiceMax;
|
|
438
|
+
if (vMax < vMin) {
|
|
439
|
+
throw new RangeError(
|
|
440
|
+
`Logical error: voiceMax (${vMax}) cannot be less than voiceMin (${vMin}).`,
|
|
441
|
+
);
|
|
426
442
|
}
|
|
427
443
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
444
|
+
const mMin = config.musicMinConsecutive ?? this.#config.musicMinConsecutive;
|
|
445
|
+
const mMax = config.musicMaxConsecutive ?? this.#config.musicMaxConsecutive;
|
|
446
|
+
if (mMax !== -1 && mMin > mMax) {
|
|
447
|
+
throw new RangeError(
|
|
448
|
+
`Logical error: musicMinConsecutive (${mMin}) cannot be greater than musicMaxConsecutive (${mMax}).`,
|
|
449
|
+
);
|
|
433
450
|
}
|
|
451
|
+
}
|
|
434
452
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
(typeof config.voiceMaxConsecutive !== 'number' || config.voiceMaxConsecutive < -1)
|
|
438
|
-
) {
|
|
439
|
-
throw new TypeError('voiceMaxConsecutive must be a number >= -1.');
|
|
440
|
-
}
|
|
453
|
+
/** @type {boolean} */
|
|
454
|
+
#optimizeMediaMemory = true;
|
|
441
455
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
456
|
+
/**
|
|
457
|
+
* Gets whether Blob URLs for media content
|
|
458
|
+
* should be converted to Base64 during export and restored during import.
|
|
459
|
+
* @returns {boolean}
|
|
460
|
+
*/
|
|
461
|
+
get optimizeMediaMemory() {
|
|
462
|
+
checkDestroy(this.#destroyed);
|
|
463
|
+
return this.#optimizeMediaMemory;
|
|
464
|
+
}
|
|
445
465
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
466
|
+
/**
|
|
467
|
+
* Sets whether Blob URLs should be converted to Base64 during export and restored during import.
|
|
468
|
+
* @param {boolean} value
|
|
469
|
+
* @throws {TypeError} If value is not a boolean.
|
|
470
|
+
*/
|
|
471
|
+
set optimizeMediaMemory(value) {
|
|
472
|
+
checkDestroy(this.#destroyed);
|
|
473
|
+
if (typeof value !== 'boolean') throw new TypeError('optimizeMediaMemory must be a boolean.');
|
|
474
|
+
this.#optimizeMediaMemory = value;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** @type {boolean} */
|
|
478
|
+
#optimizePictureMemory = true;
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Gets whether Blob URLs for pictures
|
|
482
|
+
* should be converted to Base64 during export and restored during import.
|
|
483
|
+
* @returns {boolean}
|
|
484
|
+
*/
|
|
485
|
+
get optimizePictureMemory() {
|
|
486
|
+
checkDestroy(this.#destroyed);
|
|
487
|
+
return this.#optimizePictureMemory;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Sets whether Blob URLs should be converted to Base64 during export and restored during import.
|
|
492
|
+
* @param {boolean} value
|
|
493
|
+
* @throws {TypeError} If value is not a boolean.
|
|
494
|
+
*/
|
|
495
|
+
set optimizePictureMemory(value) {
|
|
496
|
+
checkDestroy(this.#destroyed);
|
|
497
|
+
if (typeof value !== 'boolean') throw new TypeError('optimizePictureMemory must be a boolean.');
|
|
498
|
+
this.#optimizePictureMemory = value;
|
|
451
499
|
}
|
|
452
500
|
|
|
453
501
|
/**
|
|
@@ -474,6 +522,66 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
474
522
|
|
|
475
523
|
// --- PUBLIC API ---
|
|
476
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Converts new content base64 instantly to the blob url.
|
|
527
|
+
* @param {MediaContent} data
|
|
528
|
+
*/
|
|
529
|
+
_ccBase64ToBlobUrl(data) {
|
|
530
|
+
if (this.#optimizeMediaMemory) {
|
|
531
|
+
// Restores the content url if it is a Base64 string
|
|
532
|
+
if (typeof data.url === 'string' && data.url.startsWith('data:')) {
|
|
533
|
+
data.url = mediaContent.convertToBlobUrl(data.url);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (data.picture && Array.isArray(data.picture)) {
|
|
538
|
+
data.picture = data.picture.map((pic) => {
|
|
539
|
+
if (this.#optimizePictureMemory) {
|
|
540
|
+
// Check whether the data is compatible with the Blob structure (Base64 or Binary)
|
|
541
|
+
const isBase64 = typeof pic.data === 'string' && pic.data.startsWith('data:');
|
|
542
|
+
const isUint8Array = pic.data instanceof Uint8Array;
|
|
543
|
+
|
|
544
|
+
if (isBase64 || isUint8Array) {
|
|
545
|
+
return {
|
|
546
|
+
...pic,
|
|
547
|
+
data: mediaContent.convertToBlobUrl(pic.data, pic.format),
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// If memory optimization is disabled or it is a default URL, return as is
|
|
553
|
+
return pic;
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Restores the content blob instantly to the base64 url.
|
|
560
|
+
* @param {MediaContent} data
|
|
561
|
+
*/
|
|
562
|
+
async _rcblobUrlToBase64(data) {
|
|
563
|
+
// Optimizes the content url (audio/music) if it is a Blob URL
|
|
564
|
+
if (this.#optimizeMediaMemory && typeof data.url === 'string' && data.url.startsWith('blob:')) {
|
|
565
|
+
data.url = await mediaContent.blobUrlToBase64(data.url);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Optimizes the pictures
|
|
569
|
+
if (this.#optimizePictureMemory && data.picture && Array.isArray(data.picture)) {
|
|
570
|
+
data.picture = await Promise.all(
|
|
571
|
+
data.picture.map(async (pic) => {
|
|
572
|
+
// Only performs the asynchronous conversion if it really is a Blob URL
|
|
573
|
+
if (typeof pic.data === 'string' && pic.data.startsWith('blob:')) {
|
|
574
|
+
return {
|
|
575
|
+
...pic,
|
|
576
|
+
data: await mediaContent.blobUrlToBase64(pic.data),
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
return pic; // Keep intact if it is a standard URL
|
|
580
|
+
}),
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
477
585
|
/**
|
|
478
586
|
* Adds new content instantly to the radio sequence.
|
|
479
587
|
* @param {'music'|'voice'|'custom'} type - The category of the content.
|
|
@@ -495,18 +603,20 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
495
603
|
throw new TypeError('smartQueue must be a boolean.');
|
|
496
604
|
}
|
|
497
605
|
|
|
606
|
+
const newItem = structuredClone(data);
|
|
607
|
+
this._ccBase64ToBlobUrl(newItem);
|
|
498
608
|
if (type === 'music') {
|
|
499
|
-
this.#musicList.push(
|
|
609
|
+
this.#musicList.push(newItem);
|
|
500
610
|
this.#cycleCache.clear();
|
|
501
611
|
} else if (type === 'voice') {
|
|
502
|
-
this.#voiceList.push(
|
|
612
|
+
this.#voiceList.push(newItem);
|
|
503
613
|
this.#cycleCache.clear();
|
|
504
614
|
} else if (type === 'custom') {
|
|
505
|
-
this.#handleCustomInsertion(
|
|
615
|
+
this.#handleCustomInsertion(newItem, smartQueue);
|
|
506
616
|
}
|
|
507
617
|
|
|
508
618
|
this.#syncRealTimeState(Date.now());
|
|
509
|
-
this.emit('contentAdded', { type, data: structuredClone(
|
|
619
|
+
this.emit('contentAdded', { type, data: structuredClone(newItem) });
|
|
510
620
|
}
|
|
511
621
|
|
|
512
622
|
/**
|
|
@@ -541,9 +651,8 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
541
651
|
);
|
|
542
652
|
}
|
|
543
653
|
} else if (action === 'remove') {
|
|
544
|
-
if (typeof payload !== 'string')
|
|
654
|
+
if (typeof payload !== 'string')
|
|
545
655
|
throw new TypeError('Payload for "remove" must be a string (the content ID).');
|
|
546
|
-
}
|
|
547
656
|
} else if (action === 'move') {
|
|
548
657
|
if (
|
|
549
658
|
typeof payload !== 'object' ||
|
|
@@ -567,9 +676,7 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
567
676
|
|
|
568
677
|
// If there is, we set the timing of the task to the exact fraction of milliseconds
|
|
569
678
|
// in which this event ends before the next audio begins.
|
|
570
|
-
if (eventAtTime)
|
|
571
|
-
finalTimestamp = eventAtTime.absoluteEnd;
|
|
572
|
-
}
|
|
679
|
+
if (eventAtTime) finalTimestamp = eventAtTime.absoluteEnd;
|
|
573
680
|
}
|
|
574
681
|
|
|
575
682
|
/** @type {ScheduledTask} */
|
|
@@ -672,14 +779,12 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
672
779
|
*/
|
|
673
780
|
queryTimeline(targetDate, limit = 10) {
|
|
674
781
|
checkDestroy(this.#destroyed);
|
|
675
|
-
if (typeof limit !== 'number' || isNaN(limit))
|
|
676
|
-
throw new TypeError(
|
|
677
|
-
|
|
678
|
-
if (limit > this.#config.queryLimit || limit <= 0) {
|
|
782
|
+
if (typeof limit !== 'number' || isNaN(limit))
|
|
783
|
+
throw new TypeError('Invalid query limit value. Ensure it is a number.');
|
|
784
|
+
if (limit > this.#config.queryLimit || limit <= 0)
|
|
679
785
|
throw new RangeError(
|
|
680
786
|
`Invalid query limit. Ensure it is > 0 and <= ${this.#config.queryLimit}.`,
|
|
681
787
|
);
|
|
682
|
-
}
|
|
683
788
|
|
|
684
789
|
/**
|
|
685
790
|
* Virtual instance to sandbox the timeline prediction.
|
|
@@ -716,7 +821,7 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
716
821
|
}
|
|
717
822
|
|
|
718
823
|
/**
|
|
719
|
-
* Process a content list, waiting to convert the images from Blob URL to Base64.
|
|
824
|
+
* Process a content list, waiting to convert the images and content URLs from Blob URL to Base64.
|
|
720
825
|
* @param {MediaContent[]} list
|
|
721
826
|
* @returns {Promise<MediaContent[]>}
|
|
722
827
|
* @private
|
|
@@ -725,22 +830,7 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
725
830
|
return Promise.all(
|
|
726
831
|
list.map(async (item) => {
|
|
727
832
|
const newItem = structuredClone(item);
|
|
728
|
-
|
|
729
|
-
newItem.picture = await Promise.all(
|
|
730
|
-
newItem.picture.map(async (pic) => {
|
|
731
|
-
// Only performs the asynchronous conversion if it really is a Blob URL
|
|
732
|
-
if (typeof pic.data === 'string' && pic.data.startsWith('blob:')) {
|
|
733
|
-
return {
|
|
734
|
-
...pic,
|
|
735
|
-
data: await mediaContent.blobUrlToBase64(pic.data),
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
// If it is a normal URL, skip the process and keep it as it is
|
|
740
|
-
return pic;
|
|
741
|
-
}),
|
|
742
|
-
);
|
|
743
|
-
}
|
|
833
|
+
await this._rcblobUrlToBase64(newItem);
|
|
744
834
|
return newItem;
|
|
745
835
|
}),
|
|
746
836
|
);
|
|
@@ -854,12 +944,34 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
854
944
|
* @param {MediaContent[]} list - The source list to sequence.
|
|
855
945
|
* @param {number} currentSeed - Cycle-specific seed.
|
|
856
946
|
* @param {RadioModes} mode - Processing mode.
|
|
857
|
-
* @param {number} [
|
|
858
|
-
* @
|
|
947
|
+
* @param {number} [minConsecutive=0] - Min times a track must repeat.
|
|
948
|
+
* @param {number} [maxConsecutive=0] - Max times a track can repeat.
|
|
949
|
+
* @param {string|null} [initialLastId=null] - Memory of the last played ID.
|
|
950
|
+
* @param {number} [initialConsecutiveCount=0] - Memory of the consecutive count.
|
|
951
|
+
* @returns {{ items: MediaContent[], lastId: string|null, consecutiveCount: number }}
|
|
859
952
|
*/
|
|
860
|
-
#buildSequence(
|
|
861
|
-
|
|
862
|
-
|
|
953
|
+
#buildSequence(
|
|
954
|
+
list,
|
|
955
|
+
currentSeed,
|
|
956
|
+
mode,
|
|
957
|
+
minConsecutive = 0,
|
|
958
|
+
maxConsecutive = 0,
|
|
959
|
+
initialLastId = null,
|
|
960
|
+
initialConsecutiveCount = 0,
|
|
961
|
+
) {
|
|
962
|
+
if (list.length === 0) {
|
|
963
|
+
return { items: [], lastId: initialLastId, consecutiveCount: initialConsecutiveCount };
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (mode !== 'random') {
|
|
967
|
+
const items = [...list];
|
|
968
|
+
const lastItem = items[items.length - 1];
|
|
969
|
+
return {
|
|
970
|
+
items,
|
|
971
|
+
lastId: lastItem.id,
|
|
972
|
+
consecutiveCount: initialLastId === lastItem.id ? initialConsecutiveCount + 1 : 1,
|
|
973
|
+
};
|
|
974
|
+
}
|
|
863
975
|
|
|
864
976
|
/**
|
|
865
977
|
* Pool of available items for weighted selection.
|
|
@@ -875,59 +987,69 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
875
987
|
const sequence = [];
|
|
876
988
|
|
|
877
989
|
/** @type {string|null} */
|
|
878
|
-
let lastId =
|
|
879
|
-
let consecutiveCount =
|
|
990
|
+
let lastId = initialLastId;
|
|
991
|
+
let consecutiveCount = initialConsecutiveCount;
|
|
880
992
|
|
|
881
993
|
while (pool.length > 0) {
|
|
882
|
-
|
|
994
|
+
/** @type {MediaContent|null} */
|
|
995
|
+
let selectedItem = null;
|
|
996
|
+
let selectedIndex = -1;
|
|
883
997
|
|
|
884
|
-
//
|
|
885
|
-
if (
|
|
886
|
-
|
|
887
|
-
validPool = pool.filter((item) => item.id !== lastId);
|
|
998
|
+
// 1. Minimum repetition logic
|
|
999
|
+
if (lastId !== null && consecutiveCount < minConsecutive) {
|
|
1000
|
+
selectedIndex = pool.findIndex((item) => item.id === lastId);
|
|
888
1001
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
if (
|
|
892
|
-
|
|
1002
|
+
if (selectedIndex !== -1) {
|
|
1003
|
+
selectedItem = pool[selectedIndex];
|
|
1004
|
+
} else if (sequence.length > 0) {
|
|
1005
|
+
// Duplicates the previous item without consuming from pool
|
|
1006
|
+
selectedItem = sequence[sequence.length - 1];
|
|
893
1007
|
}
|
|
894
1008
|
}
|
|
895
1009
|
|
|
896
|
-
|
|
1010
|
+
// 2. Maximum repetition logic and random draw
|
|
1011
|
+
if (!selectedItem) {
|
|
1012
|
+
let validPool = pool;
|
|
897
1013
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
break;
|
|
902
|
-
}
|
|
1014
|
+
if (lastId !== null && maxConsecutive !== -1 && consecutiveCount >= maxConsecutive) {
|
|
1015
|
+
validPool = pool.filter((item) => item.id !== lastId);
|
|
1016
|
+
}
|
|
903
1017
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
let selectedItem = null;
|
|
908
|
-
let selectedIndex = -1;
|
|
1018
|
+
if (validPool.length === 0) {
|
|
1019
|
+
validPool = pool;
|
|
1020
|
+
}
|
|
909
1021
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1022
|
+
const totalWeight = validPool.reduce((sum, item) => sum + item.weight, 0);
|
|
1023
|
+
|
|
1024
|
+
if (totalWeight <= 0) {
|
|
1025
|
+
selectedItem = validPool[0];
|
|
1026
|
+
selectedIndex = pool.findIndex((p) => p.id === selectedItem?.id);
|
|
1027
|
+
} else {
|
|
1028
|
+
const r = random() * totalWeight;
|
|
1029
|
+
let sum = 0;
|
|
1030
|
+
for (let i = 0; i < validPool.length; i++) {
|
|
1031
|
+
sum += validPool[i].weight;
|
|
1032
|
+
if (r <= sum) {
|
|
1033
|
+
selectedItem = validPool[i];
|
|
1034
|
+
selectedIndex = pool.findIndex((p) => p.id === selectedItem?.id);
|
|
1035
|
+
break;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
918
1038
|
}
|
|
919
1039
|
}
|
|
920
1040
|
|
|
921
|
-
//
|
|
922
|
-
if (!selectedItem
|
|
923
|
-
selectedItem =
|
|
924
|
-
selectedIndex =
|
|
1041
|
+
// Safety fallback
|
|
1042
|
+
if (!selectedItem) {
|
|
1043
|
+
selectedItem = pool[0];
|
|
1044
|
+
selectedIndex = 0;
|
|
925
1045
|
}
|
|
926
1046
|
|
|
927
1047
|
sequence.push(selectedItem);
|
|
928
|
-
pool.splice(selectedIndex, 1);
|
|
929
1048
|
|
|
930
|
-
|
|
1049
|
+
if (selectedIndex !== -1) {
|
|
1050
|
+
pool.splice(selectedIndex, 1);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
931
1053
|
if (selectedItem.id === lastId) {
|
|
932
1054
|
consecutiveCount++;
|
|
933
1055
|
} else {
|
|
@@ -936,7 +1058,7 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
936
1058
|
}
|
|
937
1059
|
}
|
|
938
1060
|
|
|
939
|
-
return sequence;
|
|
1061
|
+
return { items: sequence, lastId, consecutiveCount };
|
|
940
1062
|
}
|
|
941
1063
|
|
|
942
1064
|
/**
|
|
@@ -948,18 +1070,44 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
948
1070
|
const cycleSeed = this.#seed + loopIndex;
|
|
949
1071
|
const mixRandom = this._prng(cycleSeed * 10);
|
|
950
1072
|
|
|
951
|
-
|
|
1073
|
+
// Initial memory state
|
|
1074
|
+
let initialLastMusicId = null;
|
|
1075
|
+
let initialMusicConsecutive = 0;
|
|
1076
|
+
let initialLastVoiceId = null;
|
|
1077
|
+
let initialVoiceConsecutive = 0;
|
|
1078
|
+
|
|
1079
|
+
// Rescues memory from the previous block seamlessly
|
|
1080
|
+
if (loopIndex > 0) {
|
|
1081
|
+
const prevBlock = this.#cycleCache.get(loopIndex - 1);
|
|
1082
|
+
if (prevBlock) {
|
|
1083
|
+
initialLastMusicId = prevBlock.lastMusicId ?? null;
|
|
1084
|
+
initialMusicConsecutive = prevBlock.musicConsecutiveCount ?? 0;
|
|
1085
|
+
initialLastVoiceId = prevBlock.lastVoiceId ?? null;
|
|
1086
|
+
initialVoiceConsecutive = prevBlock.voiceConsecutiveCount ?? 0;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const musicData = this.#buildSequence(
|
|
952
1091
|
this.#musicList,
|
|
953
1092
|
cycleSeed + 1,
|
|
954
1093
|
this.#config.mode,
|
|
1094
|
+
this.#config.musicMinConsecutive,
|
|
955
1095
|
this.#config.musicMaxConsecutive,
|
|
1096
|
+
initialLastMusicId,
|
|
1097
|
+
initialMusicConsecutive,
|
|
956
1098
|
);
|
|
957
|
-
const
|
|
1099
|
+
const musicSeq = musicData.items;
|
|
1100
|
+
|
|
1101
|
+
const voiceData = this.#buildSequence(
|
|
958
1102
|
this.#voiceList,
|
|
959
1103
|
cycleSeed + 2,
|
|
960
1104
|
this.#config.voiceMode,
|
|
1105
|
+
this.#config.voiceMinConsecutive,
|
|
961
1106
|
this.#config.voiceMaxConsecutive,
|
|
1107
|
+
initialLastVoiceId,
|
|
1108
|
+
initialVoiceConsecutive,
|
|
962
1109
|
);
|
|
1110
|
+
const voiceSeq = voiceData.items;
|
|
963
1111
|
|
|
964
1112
|
/**
|
|
965
1113
|
* Array containing the positioned items for the current cycle.
|
|
@@ -996,7 +1144,14 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
996
1144
|
}
|
|
997
1145
|
}
|
|
998
1146
|
|
|
999
|
-
return {
|
|
1147
|
+
return {
|
|
1148
|
+
items: block,
|
|
1149
|
+
duration: cycleDuration,
|
|
1150
|
+
lastMusicId: musicData.lastId,
|
|
1151
|
+
musicConsecutiveCount: musicData.consecutiveCount,
|
|
1152
|
+
lastVoiceId: voiceData.lastId,
|
|
1153
|
+
voiceConsecutiveCount: voiceData.consecutiveCount,
|
|
1154
|
+
};
|
|
1000
1155
|
}
|
|
1001
1156
|
|
|
1002
1157
|
/**
|
|
@@ -1193,9 +1348,7 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
1193
1348
|
|
|
1194
1349
|
// If there is any content playing (whether music, voice or even other custom),
|
|
1195
1350
|
// we push the initial target to the exact millisecond where it ends.
|
|
1196
|
-
if (eventAtTime)
|
|
1197
|
-
originalTarget = eventAtTime.absoluteEnd;
|
|
1198
|
-
}
|
|
1351
|
+
if (eventAtTime) originalTarget = eventAtTime.absoluteEnd;
|
|
1199
1352
|
}
|
|
1200
1353
|
|
|
1201
1354
|
const activeCps = [...this.#customPositions].sort(
|
|
@@ -1298,8 +1451,7 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
1298
1451
|
if (task.action === 'add') {
|
|
1299
1452
|
list.push(/** @type {MediaContent} */ (task.payload));
|
|
1300
1453
|
} else if (task.action === 'remove') {
|
|
1301
|
-
const
|
|
1302
|
-
const idx = list.findIndex((i) => i.id === payloadId);
|
|
1454
|
+
const idx = list.findIndex((i) => i.id === task.payload);
|
|
1303
1455
|
if (idx !== -1) {
|
|
1304
1456
|
const [removedItem] = list.splice(idx, 1);
|
|
1305
1457
|
mediaContent.revokeContentUrls(removedItem); // Free memory!
|
|
@@ -1320,9 +1472,7 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
1320
1472
|
});
|
|
1321
1473
|
}
|
|
1322
1474
|
|
|
1323
|
-
if (listsMutated)
|
|
1324
|
-
this.#cycleCache.clear();
|
|
1325
|
-
}
|
|
1475
|
+
if (listsMutated) this.#cycleCache.clear();
|
|
1326
1476
|
}
|
|
1327
1477
|
|
|
1328
1478
|
/**
|
|
@@ -1345,29 +1495,12 @@ class TinyRadioFm extends events.EventEmitter {
|
|
|
1345
1495
|
}
|
|
1346
1496
|
});
|
|
1347
1497
|
|
|
1348
|
-
const processListForImport = (/** @type {MediaContent[]} */ list) =>
|
|
1349
|
-
|
|
1350
|
-
const newItem =
|
|
1351
|
-
|
|
1352
|
-
newItem.picture = newItem.picture.map((pic) => {
|
|
1353
|
-
// Check whether the data is compatible with the Blob structure (Base64 or Binary)
|
|
1354
|
-
const isBase64 = typeof pic.data === 'string' && pic.data.startsWith('data:');
|
|
1355
|
-
const isUint8Array = pic.data instanceof Uint8Array;
|
|
1356
|
-
|
|
1357
|
-
if (isBase64 || isUint8Array) {
|
|
1358
|
-
return {
|
|
1359
|
-
...pic,
|
|
1360
|
-
data: mediaContent.convertToBlobUrl(pic.data, pic.format),
|
|
1361
|
-
};
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
// If it is a default web URL or local file, use the URL directly
|
|
1365
|
-
return pic;
|
|
1366
|
-
});
|
|
1367
|
-
}
|
|
1498
|
+
const processListForImport = (/** @type {MediaContent[]} */ list) =>
|
|
1499
|
+
list.map((item) => {
|
|
1500
|
+
const newItem = structuredClone(item);
|
|
1501
|
+
this._ccBase64ToBlobUrl(newItem);
|
|
1368
1502
|
return newItem;
|
|
1369
1503
|
});
|
|
1370
|
-
};
|
|
1371
1504
|
|
|
1372
1505
|
this.#musicList = processListForImport(data.music);
|
|
1373
1506
|
this.#voiceList = processListForImport(data.voice);
|