tiny-essentials 1.26.4 → 1.27.0

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 (120) hide show
  1. package/.github/workflows/node.js.yml +1 -1
  2. package/README.md +0 -1
  3. package/changelog/1/27/0.md +24 -0
  4. package/dist/v1/ColorSafeStringify.min.js +1 -1
  5. package/dist/v1/TinyAdvancedRaffle.min.js +1 -1
  6. package/dist/v1/TinyAfterScrollWatcher.min.js +1 -1
  7. package/dist/v1/TinyAnalogClock.min.js +1 -1
  8. package/dist/v1/TinyArrayComparator.min.js +1 -1
  9. package/dist/v1/TinyArrayPaginator.min.js +1 -1
  10. package/dist/v1/TinyBasicsEs.min.js +1 -1
  11. package/dist/v1/TinyClassManager.min.js +1 -1
  12. package/dist/v1/TinyClipboard.min.js +1 -1
  13. package/dist/v1/TinyColorConverter.min.js +1 -1
  14. package/dist/v1/TinyColorValidator.min.js +1 -1
  15. package/dist/v1/TinyCookieConsent.min.js +1 -1
  16. package/dist/v1/TinyDayNightCycle.min.js +1 -1
  17. package/dist/v1/TinyDomReadyManager.min.js +1 -1
  18. package/dist/v1/TinyDragDropDetector.min.js +1 -1
  19. package/dist/v1/TinyDragger.min.js +1 -1
  20. package/dist/v1/TinyElementObserver.min.js +1 -1
  21. package/dist/v1/TinyEssentials.min.js +1 -1
  22. package/dist/v1/TinyEvents.min.js +1 -1
  23. package/dist/v1/TinyGamepad.min.js +1 -1
  24. package/dist/v1/TinyHtml.min.js +1 -1
  25. package/dist/v1/TinyI18.min.js +1 -1
  26. package/dist/v1/TinyIframeEvents.min.js +1 -1
  27. package/dist/v1/TinyInventory.min.js +1 -1
  28. package/dist/v1/TinyInventoryTrader.min.js +1 -1
  29. package/dist/v1/TinyLevelUp.min.js +1 -1
  30. package/dist/v1/TinyLoadingScreen.min.js +1 -1
  31. package/dist/v1/TinyLocalStorage.min.js +1 -1
  32. package/dist/v1/TinyMaInSys.min.js +1 -1
  33. package/dist/v1/TinyMediaPlayer.min.js +1 -0
  34. package/dist/v1/TinyNeedBar.min.js +1 -1
  35. package/dist/v1/TinyNewWinEvents.min.js +1 -1
  36. package/dist/v1/TinyNotifications.min.js +1 -1
  37. package/dist/v1/TinyNotifyCenter.min.js +1 -1
  38. package/dist/v1/TinyPromiseQueue.min.js +1 -1
  39. package/dist/v1/TinyRadioFm.min.js +1 -0
  40. package/dist/v1/TinyRateLimiter.min.js +1 -1
  41. package/dist/v1/TinySimpleDice.min.js +1 -1
  42. package/dist/v1/TinySmartScroller.min.js +1 -1
  43. package/dist/v1/TinyTextDiffer.min.js +1 -1
  44. package/dist/v1/TinyTextRangeEditor.min.js +1 -1
  45. package/dist/v1/TinyTextarea.min.js +1 -1
  46. package/dist/v1/TinyTimeout.min.js +1 -1
  47. package/dist/v1/TinyToastNotify.min.js +1 -1
  48. package/dist/v1/TinyUploadClicker.min.js +1 -1
  49. package/dist/v1/UltraRandomMsgGen.min.js +1 -1
  50. package/dist/v1/basics/index.cjs +5 -0
  51. package/dist/v1/basics/index.d.mts +5 -1
  52. package/dist/v1/basics/index.mjs +2 -1
  53. package/dist/v1/basics/mediaContent.cjs +644 -0
  54. package/dist/v1/basics/mediaContent.d.mts +296 -0
  55. package/dist/v1/basics/mediaContent.mjs +557 -0
  56. package/dist/v1/build/TinyMediaPlayer.cjs +7 -0
  57. package/dist/v1/build/TinyMediaPlayer.d.mts +3 -0
  58. package/dist/v1/build/TinyMediaPlayer.mjs +2 -0
  59. package/dist/v1/build/TinyRadioFm.cjs +7 -0
  60. package/dist/v1/build/TinyRadioFm.d.mts +3 -0
  61. package/dist/v1/build/TinyRadioFm.mjs +2 -0
  62. package/dist/v1/index.cjs +9 -0
  63. package/dist/v1/index.d.mts +7 -1
  64. package/dist/v1/index.mjs +5 -2
  65. package/dist/v1/libs/ColorSafeStringify.d.mts +1 -1
  66. package/dist/v1/libs/TinyAfterScrollWatcher.cjs +16 -3
  67. package/dist/v1/libs/TinyAfterScrollWatcher.d.mts +7 -3
  68. package/dist/v1/libs/TinyAfterScrollWatcher.mjs +13 -3
  69. package/dist/v1/libs/TinyArrayComparator.cjs +14 -11
  70. package/dist/v1/libs/TinyArrayComparator.d.mts +22 -19
  71. package/dist/v1/libs/TinyArrayComparator.mjs +14 -11
  72. package/dist/v1/libs/TinyArrayPaginator.cjs +23 -16
  73. package/dist/v1/libs/TinyArrayPaginator.d.mts +57 -30
  74. package/dist/v1/libs/TinyArrayPaginator.mjs +22 -16
  75. package/dist/v1/libs/TinyClassManager/TinyPluginInliner.cjs +265 -0
  76. package/dist/v1/libs/TinyClassManager/TinyPluginInliner.d.mts +92 -0
  77. package/dist/v1/libs/TinyClassManager/TinyPluginInliner.mjs +223 -0
  78. package/dist/v1/libs/TinyClassManager.cjs +52 -22
  79. package/dist/v1/libs/TinyClassManager.d.mts +26 -16
  80. package/dist/v1/libs/TinyClassManager.mjs +50 -21
  81. package/dist/v1/libs/TinyColorValidator.d.mts +16 -16
  82. package/dist/v1/libs/TinyDragDropDetector.cjs +9 -34
  83. package/dist/v1/libs/TinyDragDropDetector.d.mts +7 -21
  84. package/dist/v1/libs/TinyDragDropDetector.mjs +9 -24
  85. package/dist/v1/libs/TinyDragger.cjs +5 -5
  86. package/dist/v1/libs/TinyDragger.d.mts +4 -3
  87. package/dist/v1/libs/TinyDragger.mjs +5 -5
  88. package/dist/v1/libs/TinyElementObserver.cjs +6 -21
  89. package/dist/v1/libs/TinyElementObserver.d.mts +6 -15
  90. package/dist/v1/libs/TinyElementObserver.mjs +6 -20
  91. package/dist/v1/libs/TinyGamepad.cjs +4 -6
  92. package/dist/v1/libs/TinyGamepad.d.mts +1 -1
  93. package/dist/v1/libs/TinyHtml/TinyHtmlIcon.d.mts +1 -1
  94. package/dist/v1/libs/TinyHtml/TinyHtmlTemplate.d.mts +1 -1
  95. package/dist/v1/libs/TinyHtml/TinyHtmlTextarea.d.mts +1 -1
  96. package/dist/v1/libs/TinyHtml/index.d.mts +1 -1
  97. package/dist/v1/libs/TinyHtml.cjs +72 -98
  98. package/dist/v1/libs/TinyHtml.d.mts +15 -32
  99. package/dist/v1/libs/TinyHtml.mjs +53 -77
  100. package/dist/v1/libs/TinyInventory.d.mts +1 -1
  101. package/dist/v1/libs/TinyMediaPlayer.cjs +844 -0
  102. package/dist/v1/libs/TinyMediaPlayer.d.mts +306 -0
  103. package/dist/v1/libs/TinyMediaPlayer.mjs +755 -0
  104. package/dist/v1/libs/TinyRadioFm.cjs +1378 -0
  105. package/dist/v1/libs/TinyRadioFm.d.mts +554 -0
  106. package/dist/v1/libs/TinyRadioFm.mjs +1137 -0
  107. package/dist/v1/libs/TinySmartScroller.cjs +39 -26
  108. package/dist/v1/libs/TinySmartScroller.d.mts +16 -62
  109. package/dist/v1/libs/TinySmartScroller.mjs +35 -26
  110. package/dist/v1/libs/UltraRandomMsgGen.d.mts +7 -7
  111. package/docs/v1/README.md +6 -0
  112. package/docs/v1/basics/mediaContent.md +131 -0
  113. package/docs/v1/libs/TinyClassManager/TinyPluginInliner.md +107 -0
  114. package/docs/v1/libs/TinyDragDropDetector.md +5 -9
  115. package/docs/v1/libs/TinyElementObserver.md +3 -6
  116. package/docs/v1/libs/TinyHtml.md +0 -29
  117. package/docs/v1/libs/TinyMediaPlayer.md +100 -0
  118. package/docs/v1/libs/TinyRadioFm.md +141 -0
  119. package/docs/v1/libs/TinySmartScroller.md +3 -3
  120. package/package.json +40 -22
@@ -0,0 +1,557 @@
1
+ /**
2
+ * @typedef {string|(() => string)} UnknownArtistGetter
3
+ */
4
+ /**
5
+ * Represents an image attachment template.
6
+ * @template {Uint8Array|string} PictureData
7
+ * @typedef {Object} IPictureTemplate
8
+ * @property {string} format - The MIME type of the image (e.g., 'image/jpeg').
9
+ * @property {PictureData} data - The raw binary data of the image.
10
+ * @property {string} [description] - An optional textual description of the image.
11
+ * @property {string} [type] - The specific type of picture (e.g., 'cover', 'front', 'back').
12
+ * @property {string} [name] - The filename associated with the image.
13
+ */
14
+ /**
15
+ * Represents an image attachment, such as album art.
16
+ * @typedef {IPictureTemplate<string>} IPicture
17
+ */
18
+ /**
19
+ * A numeric structure representing track or disk indexing.
20
+ * @typedef {{no: number|null, of: number|null}} MediaNumber
21
+ */
22
+ /**
23
+ * This metadata structure is modeled template.
24
+ *
25
+ * @template {IPictureTemplate<Uint8Array|string>} IPictureContent
26
+ * @typedef {Object} ContentMetadataTemplate
27
+ * @property {string|null} title - The title of the track.
28
+ * @property {string|null} album - The name of the album.
29
+ * @property {string|null} albumartist - The primary artist of the album.
30
+ * @property {string[]} albumartists - An array of artists associated with the album.
31
+ * @property {string[]} genre - An array of genres associated with the track.
32
+ * @property {string[]} label - The record label.
33
+ * @property {string[]} composer - The composer of the track.
34
+ * @property {number|null} year - The release year.
35
+ * @property {string|null} artist - The primary artist of the track.
36
+ * @property {string[]} artists - An array of artists associated with the track.
37
+ * @property {MediaNumber} disk - Disk information containing the current disk number and total disks.
38
+ * @property {MediaNumber} track - Track information containing the current track number and total tracks.
39
+ * @property {IPictureContent[]} [picture] - An array of picture objects containing album art.
40
+ */
41
+ /**
42
+ * This metadata structure is modeled after the standard output of the
43
+ * `music-metadata@11.13.0` npm package.
44
+ *
45
+ * @typedef {ContentMetadataTemplate<IPicture>} MediaContentMetadata
46
+ */
47
+ /**
48
+ * The core properties required for any content item within the media system.
49
+ * @typedef {Object} MediaContentBase
50
+ * @property {string} id - Unique identifier.
51
+ * @property {string} title - Name of the track/message.
52
+ * @property {string} artist - Artist or speaker name.
53
+ * @property {number} duration - Duration in milliseconds.
54
+ * @property {string} url - Source URL/Path.
55
+ * @property {number} [weight=1] - Probability multiplier for random selection mode.
56
+ */
57
+ /**
58
+ * The final content object used within the media, combining
59
+ * core playback properties with rich metadata.
60
+ *
61
+ * @typedef {MediaContentBase & MediaContentMetadata} MediaContent
62
+ */
63
+ //////////////////////////////////////////////////////////////////
64
+ /**
65
+ * A promise that resolves to an object containing the extracted metadata.
66
+ * @callback ParseMediaContentMetadata
67
+ * @param {Blob} data - The raw file blob.
68
+ * @returns {Promise<{ common: Partial<ContentMetadataTemplate<IPictureTemplate<Uint8Array|string>>> }>} A promise resolving to the common metadata properties.
69
+ */
70
+ /**
71
+ * @typedef {Object} LoadingMediaProgress
72
+ * @property {'loading'|'success'} status - The current status of the operation.
73
+ * @property {LoadingProgressStage} stage - The current execution stage.
74
+ * @property {ProgressEvent<EventTarget>} [event] - The current loading event.
75
+ * @property {string} url - The URL being processed.
76
+ */
77
+ /**
78
+ * @typedef {'INITIALIZING'|'DOWNLOADING'|'METADATA_LOADED'|'EXTRACTING_ID3'|'COMPLETE'} LoadingProgressStage
79
+ */
80
+ /**
81
+ * @typedef {'INITIALIZING'|'DOWNLOADING'|'METADATA'|'ID3'|'UNKNOWN'} LoadingErrorStage
82
+ */
83
+ /**
84
+ * @typedef {Object} MediaLoadingErrorData
85
+ * @property {Error} error - The original error object.
86
+ * @property {string} url - The URL that failed.
87
+ * @property {LoadingErrorStage} stage - The stage where it failed.
88
+ */
89
+ //////////////////////////////////////////////////////////////////
90
+ /** @type {Map<string, number>} */
91
+ export const _blobCounter = new Map();
92
+ /** @returns {MediaContentBase} */
93
+ export const getMediaContentBase = () => ({
94
+ id: '',
95
+ title: '',
96
+ artist: '',
97
+ url: '',
98
+ duration: 0,
99
+ weight: 1,
100
+ });
101
+ /** @returns {MediaContentMetadata} */
102
+ export const getMediaContentMetadata = () => ({
103
+ title: null,
104
+ album: null,
105
+ albumartist: null,
106
+ albumartists: [],
107
+ genre: [],
108
+ label: [],
109
+ composer: [],
110
+ year: null,
111
+ artist: null,
112
+ artists: [],
113
+ disk: { no: null, of: null },
114
+ track: { no: null, of: null },
115
+ picture: [],
116
+ });
117
+ /**
118
+ * Custom error class to provide detailed context during the content preparation process.
119
+ * @extends Error
120
+ */
121
+ export class MediaLoadingError extends Error {
122
+ /**
123
+ * @param {string} message - Human-readable error message.
124
+ * @param {string} url - The URL that caused the error.
125
+ * @param {LoadingErrorStage} stage - The stage where the error occurred.
126
+ */
127
+ constructor(message, url, stage) {
128
+ super(message);
129
+ this.name = 'MediaLoadingError';
130
+ this.url = url;
131
+ this.stage = stage;
132
+ }
133
+ }
134
+ /**
135
+ * Internal helper to generate a deterministic ID from a string.
136
+ * @param {string} str
137
+ * @returns {Promise<string>}
138
+ * @private
139
+ */
140
+ export const generateSimpleHash = async (str) => {
141
+ const msgUint8 = new TextEncoder().encode(str);
142
+ const hashBuffer = await crypto.subtle.digest('SHA-1', msgUint8);
143
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
144
+ // Return first 8 chars of the hex hash for a clean ID
145
+ return hashArray
146
+ .map((b) => b.toString(16).padStart(2, '0'))
147
+ .join('')
148
+ .substring(0, 8);
149
+ };
150
+ /**
151
+ * Helper to convert Uint8Array or Base64 string directly into a high performance Blob URL.
152
+ * @param {Uint8Array|string} data
153
+ * @param {string} format
154
+ * @returns {string} The generated Blob URL or original string if already valid.
155
+ * @private
156
+ */
157
+ export const convertToBlobUrl = (data, format = 'image/jpeg') => {
158
+ const createBlobCounter = (/** @type {Blob} */ blob) => {
159
+ const url = URL.createObjectURL(blob);
160
+ const blobUrlUsage = _blobCounter.get(url);
161
+ _blobCounter.set(url, typeof blobUrlUsage === 'number' ? blobUrlUsage + 1 : 1);
162
+ return url;
163
+ };
164
+ if (data instanceof Uint8Array) {
165
+ // @ts-ignore
166
+ const blob = new Blob([data], { type: format });
167
+ return createBlobCounter(blob);
168
+ }
169
+ else if (typeof data === 'string' && data.startsWith('data:')) {
170
+ const base64Part = data.split(',')[1];
171
+ const byteString = atob(base64Part);
172
+ const ab = new Uint8Array(byteString.length);
173
+ for (let i = 0; i < byteString.length; i++) {
174
+ ab[i] = byteString.charCodeAt(i);
175
+ }
176
+ const blob = new Blob([ab], { type: format });
177
+ return createBlobCounter(blob);
178
+ }
179
+ return typeof data === 'string' ? data : '';
180
+ };
181
+ /**
182
+ * Asynchronous helper to convert a Blob URL back to Base64 (Date URL) at export time.
183
+ * @param {string} url
184
+ * @returns {Promise<string>}
185
+ * @private
186
+ */
187
+ export const blobUrlToBase64 = async (url) => {
188
+ if (typeof url !== 'string' || !url.startsWith('blob:'))
189
+ return url;
190
+ try {
191
+ const response = await fetch(url);
192
+ const blob = await response.blob();
193
+ return await new Promise((resolve, reject) => {
194
+ const reader = new FileReader();
195
+ reader.onloadend = () => resolve(String(reader.result));
196
+ reader.onerror = reject;
197
+ reader.readAsDataURL(blob);
198
+ });
199
+ }
200
+ catch (e) {
201
+ console.warn(`[TinyAudioUtils] Failed to convert Blob URL to Base64 on export: ${url}`, e);
202
+ return url;
203
+ }
204
+ };
205
+ /**
206
+ * Safely revokes Blob URLs to prevent memory leaks from createObjectURL.
207
+ * @param {MediaContent} content
208
+ * @private
209
+ */
210
+ export const revokeContentUrls = (content) => {
211
+ if (content && Array.isArray(content.picture)) {
212
+ content.picture.forEach((pic) => {
213
+ if (typeof pic.data === 'string' && pic.data.startsWith('blob:')) {
214
+ const blobUrlUsage = _blobCounter.get(pic.data) ?? 0;
215
+ if (blobUrlUsage > 1)
216
+ _blobCounter.set(pic.data, blobUrlUsage - 1);
217
+ else
218
+ _blobCounter.delete(pic.data);
219
+ if (blobUrlUsage <= 1)
220
+ URL.revokeObjectURL(pic.data);
221
+ }
222
+ });
223
+ }
224
+ };
225
+ /**
226
+ * Central logic of metadata validation.
227
+ * @param {Partial<ContentMetadataTemplate<IPictureTemplate<string | Uint8Array<ArrayBufferLike>>>>} common - The object to be validated.
228
+ * @param {boolean} isPartial - If true, properties can be undefined.
229
+ */
230
+ const validateMediaContent = (common, isPartial) => {
231
+ // Helper to check if the property can be ignored in partial mode
232
+ const isUndefinedAllowed = (/** @type {any} */ v) => isPartial && typeof v === 'undefined';
233
+ const isString = (/** @type {string | null | undefined} */ v) => isUndefinedAllowed(v) || typeof v === 'string' || v === null;
234
+ const isNumber = (/** @type {number | null | undefined}} */ v) => isUndefinedAllowed(v) || typeof v === 'number' || v === null;
235
+ const isArray = (
236
+ /** @type {string[] | IPictureTemplate<string|Uint8Array>[] | undefined} */ v,
237
+ /** @type {(value: any, index: number, array: any[]) => any} */ valueValidator) => {
238
+ if (isUndefinedAllowed(v))
239
+ return true;
240
+ if (Array.isArray(v) && v.every(valueValidator))
241
+ return true;
242
+ return false;
243
+ };
244
+ // Validate Primitives
245
+ if (!isString(common.title))
246
+ throw new TypeError('Invalid metadata: "title" must be a string or null.');
247
+ if (!isString(common.album))
248
+ throw new TypeError('Invalid metadata: "album" must be a string or null.');
249
+ if (!isString(common.albumartist))
250
+ throw new TypeError('Invalid metadata: "albumartist" must be a string or null.');
251
+ if (!isString(common.artist))
252
+ throw new TypeError('Invalid metadata: "artist" must be a string or null.');
253
+ if (!isNumber(common.year))
254
+ throw new TypeError('Invalid metadata: "year" must be a number or null.');
255
+ // Validate Arrays
256
+ if (!isArray(common.albumartists, (value) => typeof value === 'string'))
257
+ throw new TypeError('Invalid metadata: "albumartists" must be an array of string.');
258
+ if (!isArray(common.genre, (value) => typeof value === 'string'))
259
+ throw new TypeError('Invalid metadata: "genre" must be an array of string.');
260
+ if (!isArray(common.label, (value) => typeof value === 'string'))
261
+ throw new TypeError('Invalid metadata: "label" must be an array of string.');
262
+ if (!isArray(common.composer, (value) => typeof value === 'string'))
263
+ throw new TypeError('Invalid metadata: "composer" must be an array of string.');
264
+ if (!isArray(common.artists, (value) => typeof value === 'string'))
265
+ throw new TypeError('Invalid metadata: "artists" must be an array of string.');
266
+ if (!isArray(common.picture, (value) => (typeof value.description === 'undefined' || typeof value.description === 'string') &&
267
+ (typeof value.name === 'undefined' || typeof value.name === 'string') &&
268
+ (typeof value.type === 'undefined' || typeof value.type === 'string') &&
269
+ typeof value.format === 'string' &&
270
+ (value.data instanceof Uint8Array || typeof value.data === 'string')))
271
+ throw new TypeError('Invalid metadata: "picture" must be an array of pictures.');
272
+ /**
273
+ * Validate Nested Objects (Disk and Track)
274
+ * @param {MediaNumber|null} [info]
275
+ * @param {string} [name]
276
+ */
277
+ const validateTrackInfo = (info, name) => {
278
+ if (isUndefinedAllowed(info))
279
+ return; // Ignores if partial
280
+ if (info === undefined || info === null) {
281
+ throw new TypeError(`Invalid metadata: "${name}" is required.`);
282
+ }
283
+ if (typeof info !== 'object')
284
+ throw new TypeError(`Invalid metadata: "${name}" must be an object or null.`);
285
+ if (typeof info.no !== 'number' && info.no !== null)
286
+ throw new TypeError(`Invalid metadata: "${name}.no" must be a number or null.`);
287
+ if (typeof info.of !== 'number' && info.of !== null)
288
+ throw new TypeError(`Invalid metadata: "${name}.of" must be a number or null.`);
289
+ };
290
+ validateTrackInfo(common.disk, 'disk');
291
+ validateTrackInfo(common.track, 'track');
292
+ };
293
+ /**
294
+ * Helper to validate types within the media content metadata object.
295
+ * This ensures that if a property is present, it matches the expected type.
296
+ * @param {ContentMetadataTemplate<IPictureTemplate<string | Uint8Array<ArrayBufferLike>>>} common
297
+ */
298
+ export const valMediaContentMetadata = (common) => validateMediaContent(common, false);
299
+ /**
300
+ * Helper to validate types within the media content metadata object.
301
+ * Allows the absence of properties (useful for updates/patch).
302
+ * @param {Partial<ContentMetadataTemplate<IPictureTemplate<string | Uint8Array<ArrayBufferLike>>>>} common
303
+ */
304
+ export const valMediaContentMetadataPartial = (common) => validateMediaContent(common, true);
305
+ /**
306
+ * Downloads an audio file from a URL and extracts its ID3/metadata tags.
307
+ *
308
+ * @param {string} url - The full URL of the audio file to be downloaded.
309
+ * @param {ParseMediaContentMetadata} parseFile - The function used to parse the file data.
310
+ * @returns {Promise<MediaContentMetadata>} A promise that resolves to an object containing the extracted metadata.
311
+ * @throws {TypeError} If the provided `url` is not a string or `parseFile` is not a function.
312
+ * @throws {Error} If the network request fails or the parsing process encounters an error.
313
+ */
314
+ export const extractMediaId3Tags = async (url, parseFile) => {
315
+ // Argument Validation
316
+ if (typeof url !== 'string')
317
+ throw new TypeError(`Expected url to be a string, but received ${typeof url}.`);
318
+ if (typeof parseFile !== 'function')
319
+ throw new TypeError(`Expected parseFile to be a function, but received ${typeof parseFile}.`);
320
+ try {
321
+ // 1. Download the file from the provided URL
322
+ const response = await fetch(url);
323
+ if (!response.ok)
324
+ throw new Error(`Failed to fetch audio file: ${response.status} ${response.statusText}`);
325
+ // 2. Convert the response into a Blob so the parser can read it
326
+ const blob = await response.blob();
327
+ // 3. Use the provided parser function on the Blob
328
+ const metadata = await parseFile(blob);
329
+ // 4. Complete Validation of the parsed metadata structure
330
+ if (!metadata || typeof metadata.common !== 'object')
331
+ throw new TypeError('Invalid metadata: "common" property is missing or not an object.');
332
+ const common = metadata.common;
333
+ valMediaContentMetadataPartial(common);
334
+ // 5. Return the specific metadata fields requested
335
+ // We structure the return to match the MediaContentMetadata typedef
336
+ return {
337
+ title: common?.title ?? null,
338
+ album: common?.album ?? null,
339
+ albumartist: common?.albumartist ?? null,
340
+ albumartists: common?.albumartists ?? [],
341
+ genre: common?.genre ?? [],
342
+ label: common?.label ?? [],
343
+ composer: common?.composer ?? [],
344
+ year: common?.year ?? null,
345
+ artist: common?.artist ?? null,
346
+ artists: common?.artists ?? [],
347
+ disk: common?.disk ? { no: common.disk.no, of: common.disk.of } : { no: null, of: null },
348
+ track: common?.track ? { no: common.track.no, of: common.track.of } : { no: null, of: null },
349
+ picture: common?.picture?.map((value) => ({
350
+ ...value,
351
+ data: convertToBlobUrl(value.data, value.format),
352
+ })) ?? [],
353
+ };
354
+ }
355
+ catch (error) {
356
+ // Re-throwing the error allows the caller to handle specific failure cases
357
+ throw error;
358
+ }
359
+ };
360
+ /**
361
+ * A Static Factory Method that prepares a MediaContent object by
362
+ * extracting metadata from an audio source.
363
+ *
364
+ * @param {string | HTMLMediaElement} source - A URL string or an existing Audio object.
365
+ * @param {Partial<MediaContentBase & MediaContentMetadata> & { id?: string; weight?: number }} [defaultMetadata={}] - Optional default metadata that overrides automatic extraction.
366
+ * @param {Partial<MediaContentBase & MediaContentMetadata> & { id?: string; weight?: number }} [metadata={}] - Optional manual metadata that overrides automatic extraction.
367
+ * @param {ParseMediaContentMetadata} [parseFile] - Private helper to interface with parseFile.
368
+ * @param {Object} [callbacks={}] - Callbacks for monitoring the loading process.
369
+ * @param {(progress: LoadingMediaProgress) => void} [callbacks.onProgress] - Callback triggered on stage changes.
370
+ * @param {(error: MediaLoadingErrorData) => void} [callbacks.onError] - Callback triggered when a non-fatal or fatal error occurs.
371
+ * @param {UnknownArtistGetter} unknownArtist
372
+ * @returns {Promise<MediaContent>} A promise that resolves to a valid MediaContent object.
373
+ * @throws {MediaLoadingError} If the preparation process fails at any stage.
374
+ *
375
+ * @example
376
+ * // Usage with URL
377
+ * import { parseBlob } from 'music-metadata';
378
+ * const track = await parseMediaMetadata('/assets/song.mp3', {}, { title: 'My Song', artist: 'Artist' }, parseBlob);
379
+ * media.add('music', track);
380
+ *
381
+ * @example
382
+ * // Usage with Audio Object
383
+ * import { parseBlob } from 'music-metadata';
384
+ * const audio = new Audio();
385
+ * audio.src = '/assets/song.mp3';
386
+ * const track = await parseMediaMetadata(audio, {}, {}, parseBlob);
387
+ * media.add('music', track);
388
+ *
389
+ * @example
390
+ * // Usage with tracking
391
+ * const track = await parseMediaMetadata(
392
+ * '/assets/song.mp3',
393
+ * {},
394
+ * {},
395
+ * parseBlob,
396
+ * {
397
+ * onProgress: (p) => console.log(`[${p.stage}] ${p.status}`),
398
+ * onError: (e) => console.error(`Failed at ${e.stage} for ${e.url}: ${e.error.message}`)
399
+ * }
400
+ * );
401
+ */
402
+ export const parseMediaMetadata = async (source, defaultMetadata = {}, metadata = {}, parseFile = (url) => {
403
+ return new Promise((resolve, reject) => reject(new TypeError('parseFile library not found.')));
404
+ }, callbacks = {}, unknownArtist = 'null') => {
405
+ // Argument Validation
406
+ if (typeof source !== 'string' && !(source instanceof HTMLMediaElement))
407
+ throw new TypeError('Source must be a string or an HTMLMediaElement.');
408
+ if (callbacks.onProgress && typeof callbacks.onProgress !== 'function')
409
+ throw new TypeError('callbacks.onProgress must be a function.');
410
+ if (callbacks.onError && typeof callbacks.onError !== 'function')
411
+ throw new TypeError('callbacks.onError must be a function.');
412
+ /** @type {HTMLMediaElement} */
413
+ let audio;
414
+ /** @type {string} */
415
+ let url = '';
416
+ /**
417
+ * @param {LoadingProgressStage} stage
418
+ * @param {ProgressEvent<EventTarget>} [event]
419
+ */
420
+ const notifyProgress = (stage, event) => {
421
+ if (callbacks.onProgress) {
422
+ callbacks.onProgress({
423
+ event,
424
+ status: 'loading',
425
+ stage,
426
+ url: url || (source instanceof HTMLMediaElement ? source.src : 'unknown'),
427
+ });
428
+ }
429
+ };
430
+ const notifyError = (/** @type {Error} */ error, /** @type {LoadingErrorStage} */ stage) => {
431
+ if (callbacks.onError) {
432
+ callbacks.onError({
433
+ error,
434
+ url: url || (source instanceof HTMLMediaElement ? source.src : 'unknown'),
435
+ stage,
436
+ });
437
+ }
438
+ };
439
+ try {
440
+ // 1. Normalize Source
441
+ notifyProgress('INITIALIZING');
442
+ if (typeof source === 'string') {
443
+ url = source;
444
+ audio = new Audio(url);
445
+ }
446
+ else {
447
+ audio = source;
448
+ url = audio.src;
449
+ }
450
+ // 2. Wait for audio metadata and monitor download progress
451
+ try {
452
+ await new Promise((resolve, reject) => {
453
+ /**
454
+ * Listener for the 'progress' event (detects actual data downloading)
455
+ * @type {(this: HTMLMediaElement, ev: ProgressEvent<EventTarget>) => any}
456
+ */
457
+ const onProgress = (event) => notifyProgress('DOWNLOADING', event);
458
+ // Listener for 'loadedmetadata' (duration is now available)
459
+ const onMetadata = () => {
460
+ cleanup();
461
+ resolve(undefined);
462
+ };
463
+ // Listener for errors
464
+ const onError = (/** @type {{ message: any; }} */ err) => {
465
+ cleanup();
466
+ reject(new Error(`HTMLMediaElement failed to load: ${err.message || 'Unknown error'}`));
467
+ };
468
+ const cleanup = () => {
469
+ audio.removeEventListener('progress', onProgress);
470
+ audio.removeEventListener('loadedmetadata', onMetadata);
471
+ audio.removeEventListener('error', onError);
472
+ };
473
+ audio.addEventListener('progress', onProgress);
474
+ audio.addEventListener('loadedmetadata', onMetadata);
475
+ audio.addEventListener('error', onError);
476
+ // If metadata is already there (e.g. cached by browser)
477
+ if (audio.readyState >= 1) {
478
+ cleanup();
479
+ resolve(undefined);
480
+ }
481
+ });
482
+ notifyProgress('METADATA_LOADED');
483
+ }
484
+ catch (err) {
485
+ throw new MediaLoadingError(err instanceof Error ? err.message : 'UNKNOWN ERROR', url, 'METADATA');
486
+ }
487
+ // 3. Initialize Base Data (Core properties required for the system)
488
+ const baseData = {
489
+ id: metadata.id || (await generateSimpleHash(url)),
490
+ duration: Math.floor(audio.duration * 1000), // Convert to ms for our class
491
+ url: url,
492
+ weight: metadata.weight ?? 1,
493
+ };
494
+ // 4. Automatic Metadata Extraction (ID3 Tags)
495
+ /** @type {Partial<MediaContentMetadata>} */
496
+ let extractedMetadata = {};
497
+ notifyProgress('EXTRACTING_ID3');
498
+ try {
499
+ extractedMetadata = await extractMediaId3Tags(url, parseFile);
500
+ }
501
+ catch (err) {
502
+ // We treat ID3 failure as a non-fatal error for the whole process,
503
+ // but we still notify the developer via onError.
504
+ notifyError(err instanceof Error ? err : new Error('Unknown Error'), 'ID3');
505
+ console.warn(`[TinyAudioUtils] ID3 extraction failed for ${url}. Falling back to filename.`);
506
+ }
507
+ /**
508
+ * Extracts a readable filename from a URL without the extension.
509
+ * @param {string} url
510
+ * @returns {string}
511
+ */
512
+ const getFallbackTitleFromUrl = (url) => {
513
+ try {
514
+ // Remove query params or hashes, get the last segment, and strip extension
515
+ const filename = url.split(/[?#]/)[0].split('/').pop();
516
+ return (filename ?? '').replace(/\.[^/.]+$/, '') || 'Unknown Track';
517
+ }
518
+ catch {
519
+ return 'Unknown Track';
520
+ }
521
+ };
522
+ // 5. Final Merge Logic
523
+ // Priority: Manual Metadata (highest) > Extracted ID3 > Default values
524
+ const finalContent = {
525
+ ...baseData,
526
+ ...defaultMetadata,
527
+ ...extractedMetadata,
528
+ ...metadata,
529
+ // Explicitly ensure title and artist are resolved from the hierarchy
530
+ title: extractedMetadata.title || metadata.title || getFallbackTitleFromUrl(url),
531
+ artist: extractedMetadata.artist ||
532
+ metadata.artist ||
533
+ (typeof unknownArtist === 'string' ? unknownArtist : String(unknownArtist())),
534
+ };
535
+ // Notify Success
536
+ if (callbacks.onProgress) {
537
+ callbacks.onProgress({
538
+ status: 'success',
539
+ stage: 'COMPLETE',
540
+ url: url,
541
+ });
542
+ }
543
+ return /** @type {MediaContent} */ (finalContent);
544
+ }
545
+ catch (err) {
546
+ // If it's already a MediaLoadingError, re-throw it.
547
+ // Otherwise, wrap it.
548
+ if (err instanceof MediaLoadingError) {
549
+ throw err;
550
+ }
551
+ else {
552
+ const wrappedError = new MediaLoadingError(err instanceof Error ? err.message : 'UNKNOWN ERROR', url, 'INITIALIZING');
553
+ notifyError(wrappedError, 'INITIALIZING');
554
+ throw wrappedError;
555
+ }
556
+ }
557
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ var TinyMediaPlayer = require('../libs/TinyMediaPlayer.cjs');
4
+
5
+
6
+
7
+ exports.TinyMediaPlayer = TinyMediaPlayer;
@@ -0,0 +1,3 @@
1
+ export { TinyMediaPlayer };
2
+ import TinyMediaPlayer from '../libs/TinyMediaPlayer.mjs';
3
+ //# sourceMappingURL=TinyMediaPlayer.d.mts.map
@@ -0,0 +1,2 @@
1
+ import TinyMediaPlayer from '../libs/TinyMediaPlayer.mjs';
2
+ export { TinyMediaPlayer };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ var TinyRadioFm = require('../libs/TinyRadioFm.cjs');
4
+
5
+
6
+
7
+ exports.TinyRadioFm = TinyRadioFm;
@@ -0,0 +1,3 @@
1
+ export { TinyRadioFm };
2
+ import TinyRadioFm from '../libs/TinyRadioFm.mjs';
3
+ //# sourceMappingURL=TinyRadioFm.d.mts.map
@@ -0,0 +1,2 @@
1
+ import TinyRadioFm from '../libs/TinyRadioFm.mjs';
2
+ export { TinyRadioFm };
package/dist/v1/index.cjs CHANGED
@@ -21,6 +21,7 @@ var TinyDragger = require('./libs/TinyDragger.cjs');
21
21
  var TinyDomReadyManager = require('./libs/TinyDomReadyManager.cjs');
22
22
  var TinyNotifications = require('./libs/TinyNotifications.cjs');
23
23
  var collision = require('./basics/collision.cjs');
24
+ var mediaContent = require('./basics/mediaContent.cjs');
24
25
  var TinyHtml = require('./libs/TinyHtml.cjs');
25
26
  var TinyAfterScrollWatcher = require('./libs/TinyAfterScrollWatcher.cjs');
26
27
  var UltraRandomMsgGen = require('./libs/UltraRandomMsgGen.cjs');
@@ -52,6 +53,8 @@ var TinyTextDiffer = require('./libs/TinyTextDiffer.cjs');
52
53
  var TinyArrayComparator = require('./libs/TinyArrayComparator.cjs');
53
54
  var TinyMamdaniInferenceSystem = require('./libs/TinyMamdaniInferenceSystem.cjs');
54
55
  var TinyClassManager = require('./libs/TinyClassManager.cjs');
56
+ var TinyRadioFm = require('./libs/TinyRadioFm.cjs');
57
+ var TinyMediaPlayer = require('./libs/TinyMediaPlayer.cjs');
55
58
 
56
59
 
57
60
 
@@ -154,6 +157,10 @@ exports.getElsColliding = collision.getElsColliding;
154
157
  exports.getElsPerfColliding = collision.getElsPerfColliding;
155
158
  exports.getElsRelativeCenterOffset = collision.getElsRelativeCenterOffset;
156
159
  exports.getRectCenter = collision.getRectCenter;
160
+ exports.extractMediaId3Tags = mediaContent.extractMediaId3Tags;
161
+ exports.parseMediaMetadata = mediaContent.parseMediaMetadata;
162
+ exports.valMediaContentMetadata = mediaContent.valMediaContentMetadata;
163
+ exports.valMediaContentMetadataPartial = mediaContent.valMediaContentMetadataPartial;
157
164
  exports.TinyHtml = TinyHtml;
158
165
  exports.TinyAfterScrollWatcher = TinyAfterScrollWatcher;
159
166
  exports.UltraRandomMsgGen = UltraRandomMsgGen;
@@ -188,3 +195,5 @@ exports.MamdaniInferenceSystem = TinyMamdaniInferenceSystem.MamdaniInferenceSyst
188
195
  exports.defuzzifyCentroid = TinyMamdaniInferenceSystem.defuzzifyCentroid;
189
196
  exports.trapezoid = TinyMamdaniInferenceSystem.trapezoid;
190
197
  exports.TinyClassManager = TinyClassManager;
198
+ exports.TinyRadioFm = TinyRadioFm;
199
+ exports.TinyMediaPlayer = TinyMediaPlayer;
@@ -1,3 +1,5 @@
1
+ import TinyMediaPlayer from './libs/TinyMediaPlayer.mjs';
2
+ import TinyRadioFm from './libs/TinyRadioFm.mjs';
1
3
  import TinyClassManager from './libs/TinyClassManager.mjs';
2
4
  import { FuzzySet } from './libs/TinyMamdaniInferenceSystem.mjs';
3
5
  import { MamdaniInferenceSystem } from './libs/TinyMamdaniInferenceSystem.mjs';
@@ -40,6 +42,10 @@ import TinyRateLimiter from './libs/TinyRateLimiter.mjs';
40
42
  import ColorSafeStringify from './libs/ColorSafeStringify.mjs';
41
43
  import TinyPromiseQueue from './libs/TinyPromiseQueue.mjs';
42
44
  import TinyLevelUp from './libs/TinyLevelUp.mjs';
45
+ import { valMediaContentMetadata } from './basics/mediaContent.mjs';
46
+ import { valMediaContentMetadataPartial } from './basics/mediaContent.mjs';
47
+ import { parseMediaMetadata } from './basics/mediaContent.mjs';
48
+ import { extractMediaId3Tags } from './basics/mediaContent.mjs';
43
49
  import { defuzzifyCentroid } from './libs/TinyMamdaniInferenceSystem.mjs';
44
50
  import { trapezoid } from './libs/TinyMamdaniInferenceSystem.mjs';
45
51
  import { diffArrayList } from './basics/array.mjs';
@@ -131,5 +137,5 @@ import { getTimeDuration } from './basics/clock.mjs';
131
137
  import { shuffleArray } from './basics/array.mjs';
132
138
  import { toTitleCase } from './basics/text.mjs';
133
139
  import { toTitleCaseLowerFirst } from './basics/text.mjs';
134
- export { TinyClassManager, FuzzySet, MamdaniInferenceSystem, TinyArrayComparator, TinyTextDiffer, TinyAnalogClock, TinyColorValidator, TinyLoadingScreen, TinyElementObserver, TinySimpleDice, TinyNeedBar, TinyI18, TinyCookieConsent, TinyInventory, TinyInventoryTrader, TinyArrayPaginator, TinyAdvancedRaffle, TinyDayNightCycle, TinyGamepad, TinyTextarea, TinyNewWinEvents, TinyIframeEvents, TinyLocalStorage, TinyEvents, TinyTimeout, TinyColorConverter, TinyClipboard, TinyTextRangeEditor, TinySmartScroller, UltraRandomMsgGen, TinyAfterScrollWatcher, TinyHtml, TinyNotifications, TinyDomReadyManager, TinyDragger, TinyDragDropDetector, TinyToastNotify, TinyNotifyCenter, TinyRateLimiter, ColorSafeStringify, TinyPromiseQueue, TinyLevelUp, defuzzifyCentroid, trapezoid, diffArrayList, diffStrings, breakdownDuration, calculateMarketcap, compareMarketcap, getPercentage, areElsCollTop, areElsCollBottom, areElsCollLeft, areElsCollRight, areElsCollPerfTop, areElsCollPerfBottom, areElsCollPerfLeft, areElsCollPerfRight, areElsColliding, areElsPerfColliding, getElsColliding, getElsPerfColliding, getElsCollOverlap, getElsCollOverlapPos, getRectCenter, getElsRelativeCenterOffset, getElsCollDirDepth, getElsCollDetails, safeTextTrim, installWindowHiddenScript, genFibonacciSeq, isDirEmptyAsync, fileSizeAsync, dirSizeAsync, listFilesAsync, listDirsAsync, getLatestBackupPath, fetchJson, fetchText, readJsonBlob, readFileBlob, readBase64Blob, saveJsonFile, readJsonFile, writeJsonFile, ensureDirectory, clearDirectoryAsync, clearDirectory, fileExists, dirExists, isDirEmpty, ensureCopyFile, tryDeleteFile, writeTextFile, listFiles, listDirs, fileSize, dirSize, backupFile, restoreLatestBackup, renameFileBatch, renameFileRegex, renameFileAddPrefixSuffix, renameFileNormalizeCase, renameFilePadNumbers, documentIsFullScreen, isScreenFilled, requestFullScreen, exitFullScreen, isFullScreenMode, onFullScreenChange, offFullScreenChange, isJsonObject, arraySortPositions, formatBytes, addAiMarkerShortcut, extendObjType, reorderObjTypeOrder, cloneObjTypeOrder, countObj, checkObj, objType, ruleOfThree, getSimplePerc, asyncReplace, getAge, formatCustomTimer, formatDayTimer, formatTimer, getTimeDuration, shuffleArray, toTitleCase, toTitleCaseLowerFirst };
140
+ export { TinyMediaPlayer, TinyRadioFm, TinyClassManager, FuzzySet, MamdaniInferenceSystem, TinyArrayComparator, TinyTextDiffer, TinyAnalogClock, TinyColorValidator, TinyLoadingScreen, TinyElementObserver, TinySimpleDice, TinyNeedBar, TinyI18, TinyCookieConsent, TinyInventory, TinyInventoryTrader, TinyArrayPaginator, TinyAdvancedRaffle, TinyDayNightCycle, TinyGamepad, TinyTextarea, TinyNewWinEvents, TinyIframeEvents, TinyLocalStorage, TinyEvents, TinyTimeout, TinyColorConverter, TinyClipboard, TinyTextRangeEditor, TinySmartScroller, UltraRandomMsgGen, TinyAfterScrollWatcher, TinyHtml, TinyNotifications, TinyDomReadyManager, TinyDragger, TinyDragDropDetector, TinyToastNotify, TinyNotifyCenter, TinyRateLimiter, ColorSafeStringify, TinyPromiseQueue, TinyLevelUp, valMediaContentMetadata, valMediaContentMetadataPartial, parseMediaMetadata, extractMediaId3Tags, defuzzifyCentroid, trapezoid, diffArrayList, diffStrings, breakdownDuration, calculateMarketcap, compareMarketcap, getPercentage, areElsCollTop, areElsCollBottom, areElsCollLeft, areElsCollRight, areElsCollPerfTop, areElsCollPerfBottom, areElsCollPerfLeft, areElsCollPerfRight, areElsColliding, areElsPerfColliding, getElsColliding, getElsPerfColliding, getElsCollOverlap, getElsCollOverlapPos, getRectCenter, getElsRelativeCenterOffset, getElsCollDirDepth, getElsCollDetails, safeTextTrim, installWindowHiddenScript, genFibonacciSeq, isDirEmptyAsync, fileSizeAsync, dirSizeAsync, listFilesAsync, listDirsAsync, getLatestBackupPath, fetchJson, fetchText, readJsonBlob, readFileBlob, readBase64Blob, saveJsonFile, readJsonFile, writeJsonFile, ensureDirectory, clearDirectoryAsync, clearDirectory, fileExists, dirExists, isDirEmpty, ensureCopyFile, tryDeleteFile, writeTextFile, listFiles, listDirs, fileSize, dirSize, backupFile, restoreLatestBackup, renameFileBatch, renameFileRegex, renameFileAddPrefixSuffix, renameFileNormalizeCase, renameFilePadNumbers, documentIsFullScreen, isScreenFilled, requestFullScreen, exitFullScreen, isFullScreenMode, onFullScreenChange, offFullScreenChange, isJsonObject, arraySortPositions, formatBytes, addAiMarkerShortcut, extendObjType, reorderObjTypeOrder, cloneObjTypeOrder, countObj, checkObj, objType, ruleOfThree, getSimplePerc, asyncReplace, getAge, formatCustomTimer, formatDayTimer, formatTimer, getTimeDuration, shuffleArray, toTitleCase, toTitleCaseLowerFirst };
135
141
  //# sourceMappingURL=index.d.mts.map