playlist-data-engine 1.7.2 → 1.8.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 (41) hide show
  1. package/README.md +20 -3
  2. package/bin/cli.cjs +85 -0
  3. package/dist/core/parser/TrackExtras.d.ts +150 -1
  4. package/dist/core/parser/TrackExtras.d.ts.map +1 -1
  5. package/dist/gateway-CDMPqFEH.js +1320 -0
  6. package/dist/gateway-DKa45Uz6.cjs +6 -0
  7. package/dist/gateway.d.ts +3 -1
  8. package/dist/gateway.d.ts.map +1 -1
  9. package/dist/gateway.js +1 -1
  10. package/dist/gateway.mjs +22 -14
  11. package/dist/index.d.ts +4 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/playlist-data-engine.js +34 -34
  14. package/dist/playlist-data-engine.mjs +964 -1240
  15. package/dist/utils/engineDocs.d.ts +33 -0
  16. package/dist/utils/engineDocs.d.ts.map +1 -0
  17. package/dist/utils/playlistUtils.d.ts +37 -0
  18. package/dist/utils/playlistUtils.d.ts.map +1 -1
  19. package/dist/utils/validators.d.ts +27 -0
  20. package/dist/utils/validators.d.ts.map +1 -1
  21. package/docs/DATA_ENGINE_REFERENCE.md +6660 -0
  22. package/docs/USAGE_IN_OTHER_PROJECTS.md +587 -0
  23. package/docs/features/AUDIO_ANALYSIS.md +610 -0
  24. package/docs/features/BEAT_DETECTION.md +5250 -0
  25. package/docs/features/COMBAT_SYSTEM.md +1632 -0
  26. package/docs/features/CONTENT_PACKS.md +464 -0
  27. package/docs/features/CUSTOM_CONTENT.md +603 -0
  28. package/docs/features/ENEMY_GENERATION.md +1711 -0
  29. package/docs/features/EQUIPMENT_SYSTEM.md +2279 -0
  30. package/docs/features/EXTENSIBILITY_GUIDE.md +1106 -0
  31. package/docs/features/GATEWAY_RESOLUTION.md +725 -0
  32. package/docs/features/IRL_SENSORS.md +360 -0
  33. package/docs/features/PLAYLIST_PARSING.md +446 -0
  34. package/docs/features/PREREQUISITES.md +571 -0
  35. package/docs/features/ROLLS_AND_SEEDS.md +687 -0
  36. package/docs/features/XP_AND_STATS.md +1221 -0
  37. package/llms.txt +33 -0
  38. package/package.json +9 -2
  39. package/skills/playlist-data-engine/SKILL.md +69 -0
  40. package/dist/gateway-DUk4nCao.cjs +0 -1
  41. package/dist/gateway-DyR4M-uH.js +0 -681
@@ -0,0 +1,1320 @@
1
+ import { a as $ } from "./arweaveGatewayManager-DDSO5sLJ.js";
2
+ class c {
3
+ /**
4
+ * Extract primary (compressed) audio URL with priority:
5
+ * 1. mp3_url (Standard web audio - preferred)
6
+ * 2. lossy_audio (Compressed)
7
+ * 3. audio_url (Explicit audio field)
8
+ * 4. audio (Bare audio field — some platforms use this instead of audio_url)
9
+ * 5. lossless_audio (High fidelity - larger files)
10
+ * 6. animation_url (OpenSea standard - often audio, but could be video)
11
+ * 7. multimedia_url (Alternative media field)
12
+ */
13
+ static extractAudioUrl(t) {
14
+ const e = ["mp3_url", "mp3Url", "lossy_audio", "lossyAudio", "audio_url", "audioUrl", "audio", "lossless_audio", "losslessAudio", "animation_url", "animationUrl", "multimedia_url", "multimediaUrl"];
15
+ for (const r of e)
16
+ if (t[r] && typeof t[r] == "string")
17
+ return t[r];
18
+ return null;
19
+ }
20
+ /**
21
+ * Extract the highest-fidelity audio URL the track offers — best
22
+ * available, not a guarantee. A track whose only source is an mp3 gets
23
+ * that back rather than null.
24
+ */
25
+ static extractAudioUrlLossless(t) {
26
+ const e = ["lossless_audio", "losslessAudio", "wav_url", "wavUrl", "flac_url", "flacUrl", "audio_url", "audioUrl"];
27
+ for (const r of e)
28
+ if (t[r] && typeof t[r] == "string")
29
+ return t[r];
30
+ return null;
31
+ }
32
+ /**
33
+ * Extract image URL with priority:
34
+ *
35
+ * Flat fields:
36
+ * 1. image_small (Preferred for performance)
37
+ * 2. image (Standard)
38
+ * 3. image_url (Explicit image URL variant)
39
+ * 4. image_large (Fallback)
40
+ * 5. image_uri (URI variant)
41
+ * 6. image_preview (Preview variant)
42
+ * 7. image_thumb (Last resort)
43
+ *
44
+ * Nested object fields (checked if no flat match found):
45
+ * 8. artwork.uri (Nina Protocol structured artwork)
46
+ * 9. project.artwork.uri (Project-level artwork)
47
+ * 10. primaryMedia.uri (Primary media field)
48
+ */
49
+ static extractImageUrl(t) {
50
+ const e = ["image_small", "imageSmall", "image", "image_large", "imageLarge", "image_thumb", "imageThumb", "image_url", "imageUrl", "image_uri", "imageUri", "image_preview", "imagePreview"];
51
+ for (const i of e)
52
+ if (t[i] && typeof t[i] == "string")
53
+ return t[i];
54
+ const r = [
55
+ this.extractNestedUri(t, "artwork", "uri"),
56
+ this.extractNestedUri(t, "project", "artwork", "uri"),
57
+ this.extractNestedUri(t, "primaryMedia", "uri")
58
+ ];
59
+ for (const i of r)
60
+ if (i) return i;
61
+ return null;
62
+ }
63
+ /**
64
+ * Extract thumbnail image URL directly from image_thumb fields.
65
+ * Unlike extractImageUrl, this specifically targets thumbnail fields only.
66
+ * Priority: image_thumb_url > image_thumb
67
+ */
68
+ static extractImageThumbUrl(t) {
69
+ return t.image_thumb_url && typeof t.image_thumb_url == "string" ? t.image_thumb_url : t.image_thumb && typeof t.image_thumb == "string" ? t.image_thumb : null;
70
+ }
71
+ /**
72
+ * Extract name/title with priority (see specs/001-core-engine/SPEC.md):
73
+ * 1. name
74
+ * 2. title
75
+ */
76
+ static extractTitle(t) {
77
+ return t.name && typeof t.name == "string" ? t.name : t.title && typeof t.title == "string" ? t.title : null;
78
+ }
79
+ /**
80
+ * Extract artist with priority (see specs/001-core-engine/SPEC.md):
81
+ * 1. artist
82
+ * 2. created_by
83
+ * 3. minter
84
+ * 4. OpenSea-style attributes array with trait_type "Artist"
85
+ */
86
+ static extractArtist(t) {
87
+ const e = ["artist", "artist_name", "artistName", "album_artist", "albumArtist", "created_by", "createdBy", "minter"];
88
+ for (const r of e)
89
+ if (t[r] && typeof t[r] == "string")
90
+ return t[r];
91
+ if (Array.isArray(t.attributes)) {
92
+ const r = t.attributes.find(
93
+ (i) => i.trait_type?.toLowerCase() === "artist" && i.value !== void 0
94
+ );
95
+ if (r && typeof r.value == "string")
96
+ return r.value;
97
+ }
98
+ return null;
99
+ }
100
+ /**
101
+ * Parse metadata string to JSON object
102
+ * Handles stringified JSON with error handling
103
+ */
104
+ static parseMetadata(t) {
105
+ if (!t)
106
+ return null;
107
+ if (typeof t == "object" && !Array.isArray(t))
108
+ return t;
109
+ if (typeof t == "string")
110
+ try {
111
+ const e = JSON.parse(t);
112
+ if (typeof e == "object" && !Array.isArray(e))
113
+ return e;
114
+ } catch (e) {
115
+ return console.warn("Failed to parse metadata:", e), null;
116
+ }
117
+ return null;
118
+ }
119
+ /**
120
+ * Extract genre with support for string or array formats.
121
+ * Sources are checked in priority order; first non-empty wins:
122
+ * - `genre` field as a string
123
+ * - `genre` field as an array (first element)
124
+ * - OpenSea `attributes` array with `trait_type` of "genre" (case-insensitive)
125
+ * - `attributes` as an object keyed by trait name (e.g. `attributes.Genre`)
126
+ * - malformed `attribute ` key (literal trailing space) with `.genre`
127
+ * Stays strict: only fields literally named "genre" are considered
128
+ * (no fallback to `category` or similar).
129
+ */
130
+ static extractGenre(t) {
131
+ if (typeof t.genre == "string")
132
+ return t.genre;
133
+ if (Array.isArray(t.genre) && t.genre.length > 0) {
134
+ const r = t.genre[0];
135
+ return typeof r == "string" ? r : "";
136
+ }
137
+ if (Array.isArray(t.attributes)) {
138
+ const r = t.attributes.find(
139
+ (i) => i.trait_type?.toLowerCase() === "genre" && i.value !== void 0
140
+ );
141
+ if (r) {
142
+ if (typeof r.value == "string")
143
+ return r.value;
144
+ if (Array.isArray(r.value) && r.value.length > 0) {
145
+ const i = r.value[0];
146
+ return typeof i == "string" ? i : "";
147
+ }
148
+ }
149
+ }
150
+ if (t.attributes && typeof t.attributes == "object" && !Array.isArray(t.attributes)) {
151
+ const r = t.attributes;
152
+ for (const i of Object.keys(r))
153
+ if (i.toLowerCase() === "genre" && typeof r[i] == "string")
154
+ return r[i];
155
+ }
156
+ const e = t["attribute "];
157
+ if (e && typeof e == "object" && !Array.isArray(e)) {
158
+ const r = e.genre;
159
+ if (typeof r == "string")
160
+ return r;
161
+ }
162
+ return "";
163
+ }
164
+ /**
165
+ * Extract tags from track metadata, normalized to a deduped,
166
+ * lowercased, trimmed string[] with empties removed.
167
+ * Sources (concatenated, then deduped):
168
+ * - `tags` field as an array, or a single comma-separated string
169
+ * - `properties.tags` array (when `properties` is an object)
170
+ * - OpenSea `attributes` array with `trait_type` of "tag"/"tags" (case-insensitive)
171
+ * - `attributes` as an object keyed by trait name (e.g. `attributes.Tags`)
172
+ */
173
+ static extractTags(t) {
174
+ const e = [];
175
+ if (Array.isArray(t.tags) ? e.push(...t.tags) : typeof t.tags == "string" && e.push(...t.tags.split(",")), t.properties && typeof t.properties == "object" && !Array.isArray(t.properties)) {
176
+ const r = t.properties.tags;
177
+ Array.isArray(r) ? e.push(...r) : typeof r == "string" && e.push(...r.split(","));
178
+ }
179
+ if (Array.isArray(t.attributes))
180
+ for (const r of t.attributes) {
181
+ if (!r || typeof r != "object") continue;
182
+ const i = r.trait_type;
183
+ if (typeof i == "string" && i.toLowerCase().match(/^tags?$/)) {
184
+ const n = r.value;
185
+ Array.isArray(n) ? e.push(...n) : typeof n == "string" && e.push(...n.split(","));
186
+ }
187
+ }
188
+ else if (t.attributes && typeof t.attributes == "object") {
189
+ const r = t.attributes;
190
+ for (const i of Object.keys(r))
191
+ if (i.toLowerCase().match(/^tags?$/)) {
192
+ const n = r[i];
193
+ Array.isArray(n) ? e.push(...n) : typeof n == "string" && e.push(...n.split(","));
194
+ }
195
+ }
196
+ return c.normalizeTags(e);
197
+ }
198
+ /**
199
+ * Extract track description with priority:
200
+ *
201
+ * Flat fields:
202
+ * 1. description (Canonical)
203
+ * 2. track_description / trackDescription (Explicit track-scoped variants)
204
+ * 3. description_text / descriptionText (Text-suffixed variants)
205
+ * 4. desc / blurb (Short-form variants)
206
+ *
207
+ * Then:
208
+ * 5. OpenSea-style attributes array with trait_type "description" (case-insensitive)
209
+ * 6. Deep search fallback — breadth-first walk of nested objects for the
210
+ * shallowest key matching /description/i (catches properties.description
211
+ * and any other nested home for the field). Arrays are skipped: items
212
+ * inside arrays (stems, mixes, attribute entries) carry their own scoped
213
+ * descriptions, not the track's.
214
+ */
215
+ static extractDescription(t) {
216
+ const e = ["description", "track_description", "trackDescription", "description_text", "descriptionText", "desc", "blurb"];
217
+ for (const r of e)
218
+ if (t[r] && typeof t[r] == "string")
219
+ return t[r];
220
+ if (Array.isArray(t.attributes)) {
221
+ const r = t.attributes.find(
222
+ (i) => i.trait_type?.toLowerCase() === "description" && typeof i.value == "string"
223
+ );
224
+ if (r && typeof r.value == "string")
225
+ return r.value;
226
+ }
227
+ return c.deepFindString(t, /description/i);
228
+ }
229
+ /**
230
+ * Extract album description — one of the bonus flavors. Priority:
231
+ *
232
+ * Flat fields:
233
+ * 1. album_description (Canonical)
234
+ * 2. albumDescription (camelCase variant)
235
+ *
236
+ * Nested object fields (only when `album` is an object — it is just as
237
+ * often a plain name string, which yields nothing here):
238
+ * 3. Deep search scoped to the album object — catches album.description,
239
+ * album.liner_notes, album.notes and friends without ever misattributing
240
+ * the track's own description to the album.
241
+ */
242
+ static extractAlbumDescription(t) {
243
+ const e = ["album_description", "albumDescription"];
244
+ for (const i of e)
245
+ if (t[i] && typeof t[i] == "string")
246
+ return t[i];
247
+ const r = t.album;
248
+ return r && typeof r == "object" && !Array.isArray(r) ? c.deepFindString(r, /description|liner|notes/i) : null;
249
+ }
250
+ /**
251
+ * Extract artist description — one of the bonus flavors. Priority:
252
+ *
253
+ * Flat fields:
254
+ * 1. artist_description (Canonical)
255
+ * 2. artistDescription (camelCase variant)
256
+ * 3. artist_bio / artistBio (Bio variants)
257
+ *
258
+ * Nested object fields (only when `artist` is an object — it is just as
259
+ * often a plain name string, which yields nothing here):
260
+ * 4. Deep search scoped to the artist object — catches artist.description,
261
+ * artist.bio, artist.about and friends without ever misattributing the
262
+ * track's own description to the artist.
263
+ */
264
+ static extractArtistDescription(t) {
265
+ const e = ["artist_description", "artistDescription", "artist_bio", "artistBio"];
266
+ for (const i of e)
267
+ if (t[i] && typeof t[i] == "string")
268
+ return t[i];
269
+ const r = t.artist;
270
+ return r && typeof r == "object" && !Array.isArray(r) ? c.deepFindString(r, /description|bio|about/i) : null;
271
+ }
272
+ /**
273
+ * Breadth-first search for the shallowest string value whose key matches
274
+ * the given pattern. First match in breadth order wins: shallowest level,
275
+ * then key order. Objects are descended (arrays skipped), depth-capped so
276
+ * pathological metadata blobs stay cheap.
277
+ */
278
+ static deepFindString(t, e, r = 5) {
279
+ const i = /* @__PURE__ */ new Set(), n = [{ node: t, depth: 0 }];
280
+ for (; n.length > 0; ) {
281
+ const { node: u, depth: o } = n.shift();
282
+ for (const a of Object.keys(u)) {
283
+ const l = u[a];
284
+ if (typeof l == "string" && l && e.test(a))
285
+ return l;
286
+ }
287
+ if (!(o >= r))
288
+ for (const a of Object.keys(u)) {
289
+ const l = u[a];
290
+ l && typeof l == "object" && !Array.isArray(l) && !i.has(l) && (i.add(l), n.push({ node: l, depth: o + 1 }));
291
+ }
292
+ }
293
+ return null;
294
+ }
295
+ /**
296
+ * Normalize a list of raw tag values into a deduped, lowercased,
297
+ * trimmed string[] with empties removed.
298
+ */
299
+ static normalizeTags(t) {
300
+ const e = /* @__PURE__ */ new Set();
301
+ for (const r of t) {
302
+ if (r == null) continue;
303
+ const i = String(r).trim().toLowerCase();
304
+ i && e.add(i);
305
+ }
306
+ return Array.from(e);
307
+ }
308
+ static convertAttributes(t) {
309
+ if (!Array.isArray(t))
310
+ return null;
311
+ const e = {};
312
+ for (const r of t)
313
+ r.trait_type && r.value !== void 0 && (e[r.trait_type] = r.value);
314
+ return Object.keys(e).length > 0 ? e : null;
315
+ }
316
+ /**
317
+ * Safely extract a URI string from a nested object path.
318
+ * e.g. extractNestedUri(data, 'project', 'artwork', 'uri') => data.project.artwork.uri
319
+ */
320
+ static extractNestedUri(t, ...e) {
321
+ let r = t;
322
+ for (const i of e)
323
+ if (r && typeof r == "object" && !Array.isArray(r))
324
+ r = r[i];
325
+ else
326
+ return null;
327
+ return typeof r == "string" ? r : null;
328
+ }
329
+ }
330
+ function _e(s) {
331
+ if (!s || !s.metadata) return null;
332
+ if (typeof s.metadata == "string")
333
+ try {
334
+ const t = JSON.parse(s.metadata);
335
+ return typeof t == "object" && t !== null && !Array.isArray(t) ? t : null;
336
+ } catch {
337
+ return null;
338
+ }
339
+ return typeof s.metadata == "object" && !Array.isArray(s.metadata) ? s.metadata : null;
340
+ }
341
+ function A(s) {
342
+ const t = { stems: [], mixes: [], hasExtras: !1 };
343
+ if (!s) return t;
344
+ const e = V(s), r = Z(s), i = typeof s.vrm == "string" ? s.vrm : void 0, n = K(s.lyrics), u = S(s.visualizer), o = S(s.video), a = Q(s.merch), l = J(s.credits), f = typeof s.midi == "string" ? s.midi : void 0, d = typeof s.step_mania == "string" ? s.step_mania : void 0, m = typeof s.clone_hero == "string" ? s.clone_hero : void 0, p = typeof s.external_url == "string" ? s.external_url : void 0;
345
+ return {
346
+ hasExtras: e.length > 0 || r.length > 0 || !!i || !!n || !!u || !!o || !!a || !!l || !!f || !!d || !!m || !!p,
347
+ ...e.length > 0 ? { stems: e } : {},
348
+ ...r.length > 0 ? { mixes: r } : {},
349
+ ...i ? { vrm: i } : {},
350
+ ...n ? { lyrics: n } : {},
351
+ ...u ? { visualizer: u } : {},
352
+ ...o ? { video: o } : {},
353
+ ...a ? { merch: a } : {},
354
+ ...l ? { credits: l } : {},
355
+ ...f ? { midi: f } : {},
356
+ ...d ? { step_mania: d } : {},
357
+ ...m ? { clone_hero: m } : {},
358
+ ...p ? { external_url: p } : {}
359
+ };
360
+ }
361
+ const w = ["audio/wav", "audio/x-wav", "audio/flac"];
362
+ function k(s, t, e) {
363
+ const r = t?.["Selected Mix"], i = typeof s == "string" && s || typeof r == "string" && r || null;
364
+ if (!i || i === "default") return null;
365
+ const n = (e ?? []).filter((l) => l.name === i);
366
+ if (n.length === 0) return null;
367
+ const u = (l) => w.includes((l.mime_type ?? "").toLowerCase()), o = n.find(u), a = n.find((l) => !u(l)) ?? n[0];
368
+ return {
369
+ name: i,
370
+ ...a?.uri ? { audio_url: a.uri } : {},
371
+ ...o?.uri && o.uri !== a?.uri ? { audio_url_lossless: o.uri } : {}
372
+ };
373
+ }
374
+ function ve(s, t, e) {
375
+ if (!s || !t) return null;
376
+ const r = C(s, t, e?.caseInsensitive !== !1, e?.aliases !== !1);
377
+ return r.length > 0 ? I(r, e?.prefer ?? "lossy") : null;
378
+ }
379
+ function I(s, t = "lossy") {
380
+ if (!s || s.length === 0) return null;
381
+ if (s.length === 1) return s[0];
382
+ const e = t === "lossy" ? ["audio/mpeg", "audio/wav", "audio/flac"] : ["audio/wav", "audio/flac", "audio/mpeg"];
383
+ for (const r of e) {
384
+ const i = s.find((n) => M(n.mime_type) === r);
385
+ if (i) return i;
386
+ }
387
+ return s[0];
388
+ }
389
+ function Ae(s) {
390
+ if (!s || !Array.isArray(s)) return [];
391
+ const t = /* @__PURE__ */ new Map();
392
+ for (const e of s) {
393
+ const r = e.name.trim().toLowerCase(), i = t.get(r);
394
+ i ? i.push(e) : t.set(r, [e]);
395
+ }
396
+ return Array.from(t.values()).map((e) => ({
397
+ name: e[0].name,
398
+ mixes: e
399
+ }));
400
+ }
401
+ async function b(s, t) {
402
+ if (!s) return null;
403
+ const e = s, r = e.uri ?? e.audio_url ?? e.audio_url_lossless;
404
+ if (!r) return null;
405
+ let i = r;
406
+ if (t?.followMetadata !== !1 && X(s, r)) {
407
+ const n = await q(r, t?.fetchTimeoutMs);
408
+ if (!n) return null;
409
+ i = n;
410
+ }
411
+ if (t?.resolveUrl === !1) return i;
412
+ try {
413
+ return await $.resolveUrl(i);
414
+ } catch {
415
+ return null;
416
+ }
417
+ }
418
+ async function xe(s, t, e) {
419
+ const r = s.extras?.mixes;
420
+ if (!r || r.length === 0 || !t) return null;
421
+ let i = C(r, t, !0, e?.aliases !== !1);
422
+ if (i.length === 0) return null;
423
+ if (e?.conditionsContext) {
424
+ const f = re(
425
+ { hasExtras: s.extras?.hasExtras ?? !0, mixes: r },
426
+ e.conditionsContext
427
+ ), d = new Set(f.filter((m) => m.allMet).map((m) => m.mix.name));
428
+ if (i = i.filter((m) => d.has(m.name)), i.length === 0) return null;
429
+ }
430
+ const n = I(i, e?.prefer ?? "lossy");
431
+ if (!n || !n.uri) return null;
432
+ const u = {
433
+ resolveUrl: e?.resolveUrl,
434
+ followMetadata: e?.followMetadata
435
+ }, o = await b(n, u);
436
+ if (!o) return null;
437
+ const a = i.find(W), l = a && a !== n && a.uri ? await b(a, u) : void 0;
438
+ return {
439
+ ...s,
440
+ audio_url: o,
441
+ audio_url_lossless: l && l !== o ? l : void 0,
442
+ selected_mix: n.name
443
+ };
444
+ }
445
+ const j = ["application/json", "application/ld+json"], B = {
446
+ "audio/x-wav": "audio/wav",
447
+ "audio/wave": "audio/wav",
448
+ "audio/vnd.wave": "audio/wav",
449
+ "audio/x-flac": "audio/flac",
450
+ "audio/mp3": "audio/mpeg",
451
+ "audio/mpeg3": "audio/mpeg"
452
+ };
453
+ function M(s) {
454
+ const t = (s ?? "").trim().toLowerCase();
455
+ return B[t] ?? t;
456
+ }
457
+ function W(s) {
458
+ return w.includes(M(s.mime_type));
459
+ }
460
+ function H(s, t, e) {
461
+ if (!s || !t) return [];
462
+ const r = (n) => e ? n.trim().toLowerCase() : n.trim(), i = r(t);
463
+ return s.filter((n) => r(n.name) === i);
464
+ }
465
+ const Y = [
466
+ ["no vocals", "instrumental"],
467
+ ["vocals only", "acapella"],
468
+ ["vocal only", "acapella"],
469
+ ["a cappella", "acapella"],
470
+ ["tv mix", "karaoke"],
471
+ ["tv version", "karaoke"],
472
+ ["karaoke version", "karaoke"],
473
+ ["karaoke mix", "karaoke"]
474
+ ], z = {
475
+ inst: "instrumental",
476
+ instr: "instrumental",
477
+ instrument: "instrumental",
478
+ instrumentals: "instrumental",
479
+ aca: "acapella",
480
+ acap: "acapella",
481
+ acappella: "acapella",
482
+ cappella: "acapella",
483
+ vox: "acapella",
484
+ vocal: "acapella",
485
+ vocals: "acapella",
486
+ rmx: "remix",
487
+ tvmix: "karaoke",
488
+ tv: "karaoke",
489
+ karoake: "karaoke"
490
+ };
491
+ function E(s) {
492
+ let t = " " + s.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim() + " ";
493
+ for (const [e, r] of Y)
494
+ t = t.split(" " + e + " ").join(" " + r + " ");
495
+ return t.trim().split(" ").filter(Boolean).map((e) => z[e] ?? e);
496
+ }
497
+ function C(s, t, e, r) {
498
+ const i = H(s, t, e);
499
+ if (i.length > 0 || !r || !e) return i;
500
+ const n = E(t);
501
+ return n.length === 0 ? [] : s.filter((u) => {
502
+ const o = E(u.name);
503
+ return n.every((a) => o.includes(a));
504
+ });
505
+ }
506
+ function X(s, t) {
507
+ const e = ("mime_type" in s ? s.mime_type : "") ?? "";
508
+ return j.includes(e.toLowerCase()) ? !0 : e ? !1 : t.split(/[?#]/)[0].toLowerCase().endsWith(".json");
509
+ }
510
+ async function q(s, t = 1e4) {
511
+ const e = new AbortController(), r = setTimeout(() => e.abort(), t);
512
+ try {
513
+ const i = await fetch(s, { signal: e.signal });
514
+ if (!i.ok) return null;
515
+ const n = c.parseMetadata(await i.text());
516
+ return n ? c.extractAudioUrl(n) : null;
517
+ } catch {
518
+ return null;
519
+ } finally {
520
+ clearTimeout(r);
521
+ }
522
+ }
523
+ function K(s) {
524
+ if (s) {
525
+ if (typeof s == "string") return { text: s };
526
+ if (typeof s == "object" && !Array.isArray(s)) {
527
+ const t = s;
528
+ return { text: typeof t.text == "string" ? t.text : void 0 };
529
+ }
530
+ }
531
+ }
532
+ function S(s) {
533
+ if (!s || typeof s != "object" || Array.isArray(s)) return;
534
+ const t = s;
535
+ if (typeof t.uri == "string")
536
+ return {
537
+ uri: t.uri,
538
+ mime_type: typeof t.mime_type == "string" ? t.mime_type : void 0
539
+ };
540
+ }
541
+ function Q(s) {
542
+ if (!s || typeof s != "object" || Array.isArray(s)) return;
543
+ const t = s;
544
+ if (typeof t.uri == "string")
545
+ return {
546
+ uri: t.uri,
547
+ mime_type: typeof t.mime_type == "string" ? t.mime_type : void 0,
548
+ type: typeof t.type == "string" ? t.type : void 0
549
+ };
550
+ }
551
+ function J(s) {
552
+ if (!Array.isArray(s)) return;
553
+ const t = [];
554
+ for (const e of s)
555
+ if (e && typeof e == "object") {
556
+ const r = e;
557
+ typeof r.name == "string" && typeof r.credit == "string" && t.push({ name: r.name, credit: r.credit });
558
+ }
559
+ return t.length > 0 ? t : void 0;
560
+ }
561
+ function V(s) {
562
+ const t = s.stems;
563
+ if (!Array.isArray(t)) return [];
564
+ const e = [];
565
+ for (const r of t)
566
+ if (r && typeof r == "object") {
567
+ const i = r;
568
+ e.push({
569
+ name: typeof i.name == "string" ? i.name : "",
570
+ uri: typeof i.uri == "string" ? i.uri : void 0,
571
+ mime_type: typeof i.mime_type == "string" ? i.mime_type : void 0
572
+ });
573
+ }
574
+ return e;
575
+ }
576
+ function Z(s) {
577
+ const t = s.mixes;
578
+ if (!Array.isArray(t)) return [];
579
+ const e = [];
580
+ for (const r of t)
581
+ if (r && typeof r == "object") {
582
+ const i = r, n = ee(i.name), u = te(i.conditions);
583
+ e.push({
584
+ name: n || "",
585
+ uri: typeof i.uri == "string" ? i.uri : void 0,
586
+ mime_type: typeof i.mime_type == "string" ? i.mime_type : void 0,
587
+ conditions: u
588
+ });
589
+ }
590
+ return e;
591
+ }
592
+ function ee(s) {
593
+ if (typeof s == "string") return s;
594
+ if (s && typeof s == "object" && !Array.isArray(s)) {
595
+ const t = s;
596
+ if (typeof t.value == "string") return t.value;
597
+ }
598
+ return "";
599
+ }
600
+ function te(s) {
601
+ if (!Array.isArray(s)) return [];
602
+ const t = [];
603
+ for (const e of s)
604
+ if (e && typeof e == "object") {
605
+ const r = e;
606
+ typeof r.type == "string" && (typeof r.value == "string" || typeof r.value == "number" || typeof r.value == "boolean") && t.push({ type: r.type, value: String(r.value) });
607
+ }
608
+ return t;
609
+ }
610
+ function re(s, t) {
611
+ const e = [], r = /* @__PURE__ */ new Date();
612
+ for (const i of s.mixes || []) {
613
+ if (i.conditions.length === 0) {
614
+ e.push({
615
+ mix: i,
616
+ conditions: [],
617
+ allMet: !0,
618
+ unmetConditions: []
619
+ });
620
+ continue;
621
+ }
622
+ const n = [], u = [];
623
+ for (const o of i.conditions) {
624
+ const a = se(o, t, r);
625
+ n.push(a), a.met || u.push(a);
626
+ }
627
+ e.push({
628
+ mix: i,
629
+ conditions: n,
630
+ allMet: u.length === 0,
631
+ unmetConditions: u
632
+ });
633
+ }
634
+ return e;
635
+ }
636
+ function se(s, t, e = /* @__PURE__ */ new Date()) {
637
+ const { type: r, value: i } = s, n = t?.environment, u = t?.appState;
638
+ switch (r) {
639
+ case "weather": {
640
+ const o = n?.weather?.weatherType || "", a = o.toLowerCase() === i.toLowerCase();
641
+ return {
642
+ type: r,
643
+ value: i,
644
+ met: a,
645
+ reason: a ? `Weather is ${o}` : `Weather is ${o} (need ${i})`
646
+ };
647
+ }
648
+ case "day": {
649
+ const a = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][e.getDay()], l = a.toLowerCase() === i.toLowerCase();
650
+ return {
651
+ type: r,
652
+ value: i,
653
+ met: l,
654
+ reason: l ? `Today is ${a}` : `Today is ${a} (need ${i})`
655
+ };
656
+ }
657
+ case "start_time": {
658
+ const o = e.getHours() * 60 + e.getMinutes(), a = T(i), l = o >= a;
659
+ return {
660
+ type: r,
661
+ value: i,
662
+ met: l,
663
+ reason: l ? `Time is after ${i}` : `Time is before ${i}`
664
+ };
665
+ }
666
+ case "end_time": {
667
+ const o = e.getHours() * 60 + e.getMinutes(), a = T(i), l = o < a;
668
+ return {
669
+ type: r,
670
+ value: i,
671
+ met: l,
672
+ reason: l ? `Time is before ${i}` : `Time is after ${i}`
673
+ };
674
+ }
675
+ case "min_plays": {
676
+ const o = parseInt(i, 10), a = u?.playCount ?? 0, l = a >= o;
677
+ return {
678
+ type: r,
679
+ value: i,
680
+ met: l,
681
+ reason: l ? `Played ${a} times (need ${o})` : `Played ${a} times (need ${o})`
682
+ };
683
+ }
684
+ case "max_plays": {
685
+ const o = parseInt(i, 10), a = u?.playCount ?? 0, l = a <= o;
686
+ return {
687
+ type: r,
688
+ value: i,
689
+ met: l,
690
+ reason: l ? `Played ${a} times (max ${o})` : `Played ${a} times (exceeds max ${o})`
691
+ };
692
+ }
693
+ case "every_x_plays": {
694
+ const o = parseInt(i, 10), a = u?.playCount ?? 0, l = o > 0 && a > 0 && a % o === 0;
695
+ return {
696
+ type: r,
697
+ value: i,
698
+ met: l,
699
+ reason: l ? `Play #${a} (every ${o} plays)` : `Play #${a} (not a multiple of ${o})`
700
+ };
701
+ }
702
+ case "altitude": {
703
+ const o = n?.geolocation?.altitude;
704
+ if (o == null)
705
+ return { type: r, value: i, met: !1, reason: "Altitude unavailable" };
706
+ const a = ie(i), l = ne(o, a.operator, a.threshold);
707
+ return {
708
+ type: r,
709
+ value: i,
710
+ met: l,
711
+ reason: l ? `Altitude ${o}m ${a.operator} ${a.threshold}m` : `Altitude ${o}m (need ${a.operator} ${a.threshold}m)`
712
+ };
713
+ }
714
+ case "favorite": {
715
+ const o = u?.isFavorite ?? !1, a = i.toLowerCase() === "true", l = o === a;
716
+ return {
717
+ type: r,
718
+ value: i,
719
+ met: l,
720
+ reason: l ? o ? "Track is favorited" : "Track is not favorited" : o ? "Track is favorited (need unfavorited)" : "Track is not favorited (need favorited)"
721
+ };
722
+ }
723
+ case "birthday": {
724
+ if (!u?.userBirthday)
725
+ return { type: r, value: i, met: !1, reason: "Birthday not set" };
726
+ const a = `${String(e.getMonth() + 1).padStart(2, "0")}-${String(e.getDate()).padStart(2, "0")}` === u.userBirthday;
727
+ return {
728
+ type: r,
729
+ value: i,
730
+ met: a,
731
+ reason: a ? "It's your birthday!" : `Not your birthday (need ${u.userBirthday})`
732
+ };
733
+ }
734
+ case "weight":
735
+ return {
736
+ type: r,
737
+ value: i,
738
+ met: !0,
739
+ reason: `Weight ${i} (used for random selection, always available)`
740
+ };
741
+ default:
742
+ return {
743
+ type: r,
744
+ value: i,
745
+ met: !0,
746
+ reason: `Custom condition: ${r} = ${i}`
747
+ };
748
+ }
749
+ }
750
+ function T(s) {
751
+ const t = s.split(":");
752
+ if (t.length !== 2) return 0;
753
+ const e = parseInt(t[0], 10), r = parseInt(t[1], 10);
754
+ return isNaN(e) || isNaN(r) ? 0 : e * 60 + r;
755
+ }
756
+ function ie(s) {
757
+ const t = s.match(/^(>=|<=|>|<|=)?\s*(\d+(?:\.\d+)?)\s*$/);
758
+ return t ? {
759
+ operator: t[1] || ">=",
760
+ threshold: parseFloat(t[2])
761
+ } : { operator: ">=", threshold: parseFloat(s) || 0 };
762
+ }
763
+ function ne(s, t, e) {
764
+ switch (t) {
765
+ case ">":
766
+ return s > e;
767
+ case ">=":
768
+ return s >= e;
769
+ case "<":
770
+ return s < e;
771
+ case "<=":
772
+ return s <= e;
773
+ case "=":
774
+ case "==":
775
+ return s === e;
776
+ default:
777
+ return s >= e;
778
+ }
779
+ }
780
+ function h(s) {
781
+ if ("audio_url" in s && typeof s.audio_url == "string")
782
+ return s.audio_url;
783
+ if ("metadata" in s) {
784
+ const t = c.parseMetadata(s.metadata);
785
+ if (t)
786
+ return c.extractAudioUrl(t);
787
+ }
788
+ return null;
789
+ }
790
+ function N(s) {
791
+ if ("audio_url_lossless" in s && typeof s.audio_url_lossless == "string")
792
+ return s.audio_url_lossless;
793
+ if ("metadata" in s) {
794
+ const t = c.parseMetadata(s.metadata);
795
+ if (t)
796
+ return c.extractAudioUrlLossless(t);
797
+ }
798
+ return null;
799
+ }
800
+ function _(s) {
801
+ if ("image_url" in s && typeof s.image_url == "string")
802
+ return s.image_url;
803
+ if ("metadata" in s) {
804
+ if ("artwork_url" in s && typeof s.artwork_url == "string")
805
+ return s.artwork_url;
806
+ const t = c.parseMetadata(s.metadata);
807
+ if (t)
808
+ return c.extractImageUrl(t);
809
+ }
810
+ return null;
811
+ }
812
+ function P(s) {
813
+ if ("image_thumb_url" in s && typeof s.image_thumb_url == "string")
814
+ return s.image_thumb_url;
815
+ if ("metadata" in s) {
816
+ const t = c.parseMetadata(s.metadata);
817
+ if (t)
818
+ return c.extractImageThumbUrl(t);
819
+ }
820
+ return null;
821
+ }
822
+ function L(s) {
823
+ if ("metadata" in s) {
824
+ const t = c.parseMetadata(s.metadata);
825
+ if (t && typeof t.vrm == "string" && t.vrm)
826
+ return t.vrm;
827
+ }
828
+ return "extras" in s && s.extras && typeof s.extras.vrm == "string" && s.extras.vrm || null;
829
+ }
830
+ function U(s) {
831
+ if ("extras" in s && s.extras?.mixes)
832
+ return s.extras.mixes;
833
+ if ("metadata" in s) {
834
+ const t = c.parseMetadata(s.metadata);
835
+ if (t)
836
+ return A(t).mixes ?? [];
837
+ }
838
+ return [];
839
+ }
840
+ function R(s) {
841
+ if ("audio_url" in s || !("metadata" in s))
842
+ return null;
843
+ const t = c.parseMetadata(s.metadata);
844
+ return k(
845
+ s.selected_mix,
846
+ c.convertAttributes(t?.attributes),
847
+ t ? A(t).mixes : void 0
848
+ );
849
+ }
850
+ function be(s) {
851
+ const t = [];
852
+ for (const e of s.tracks) {
853
+ const r = h(e);
854
+ r && t.push(r);
855
+ }
856
+ return t;
857
+ }
858
+ function Ee(s) {
859
+ const t = [];
860
+ for (const e of s.tracks) {
861
+ const r = _(e);
862
+ r && t.push(r);
863
+ }
864
+ return t;
865
+ }
866
+ function Se(s) {
867
+ const t = [];
868
+ for (const e of s.tracks) {
869
+ if ("title" in e && typeof e.title == "string") {
870
+ t.push(e.title);
871
+ continue;
872
+ }
873
+ if ("metadata" in e) {
874
+ const r = c.parseMetadata(e.metadata);
875
+ if (r) {
876
+ const i = c.extractTitle(r);
877
+ i && t.push(i);
878
+ }
879
+ }
880
+ }
881
+ return t;
882
+ }
883
+ function Te(s) {
884
+ const t = [];
885
+ for (const e of s.tracks) {
886
+ if ("artist" in e && typeof e.artist == "string") {
887
+ t.push(e.artist);
888
+ continue;
889
+ }
890
+ if ("metadata" in e) {
891
+ const r = c.parseMetadata(e.metadata);
892
+ if (r) {
893
+ const i = c.extractArtist(r);
894
+ i && t.push(i);
895
+ }
896
+ }
897
+ }
898
+ return t;
899
+ }
900
+ function we(s) {
901
+ const t = /* @__PURE__ */ new Set();
902
+ for (const e of s.tracks) {
903
+ if ("genre" in e && typeof e.genre == "string" && e.genre) {
904
+ t.add(e.genre);
905
+ continue;
906
+ }
907
+ if ("metadata" in e) {
908
+ const r = c.parseMetadata(e.metadata);
909
+ r && typeof r.genre == "string" && r.genre && t.add(r.genre);
910
+ }
911
+ }
912
+ return Array.from(t).sort();
913
+ }
914
+ function ke(s) {
915
+ const t = /* @__PURE__ */ new Set();
916
+ for (const e of s.tracks) {
917
+ if ("tags" in e && Array.isArray(e.tags)) {
918
+ for (const r of e.tags)
919
+ r && t.add(r.toLowerCase());
920
+ continue;
921
+ }
922
+ if ("metadata" in e) {
923
+ const r = c.parseMetadata(e.metadata);
924
+ if (r && Array.isArray(r.tags))
925
+ for (const i of r.tags)
926
+ i && t.add(String(i).toLowerCase());
927
+ }
928
+ }
929
+ return Array.from(t).sort();
930
+ }
931
+ function Ie(s) {
932
+ let t = 0;
933
+ for (const e of s.tracks) {
934
+ if ("duration" in e && typeof e.duration == "number") {
935
+ t += e.duration;
936
+ continue;
937
+ }
938
+ if ("metadata" in e) {
939
+ const r = c.parseMetadata(e.metadata);
940
+ r && typeof r.duration == "number" && (t += r.duration);
941
+ }
942
+ }
943
+ return t;
944
+ }
945
+ function Me(s) {
946
+ return s.tracks.length;
947
+ }
948
+ function Ce(s) {
949
+ const t = [];
950
+ for (const e of s.tracks) {
951
+ let r = h(e);
952
+ const i = _(e), n = P(e), u = R(e);
953
+ u?.audio_url && (r = u.audio_url);
954
+ const o = u ? u.name : "audio_url" in e ? e.selected_mix : void 0;
955
+ let a = "", l = "", f;
956
+ if ("title" in e)
957
+ a = e.title || "", l = e.artist || "", f = e.description || void 0;
958
+ else if ("metadata" in e) {
959
+ const d = c.parseMetadata(e.metadata);
960
+ d && (a = c.extractTitle(d) || "", l = c.extractArtist(d) || "", f = c.extractDescription(d) || void 0);
961
+ }
962
+ if (r) {
963
+ const d = {
964
+ title: a,
965
+ artist: l,
966
+ audio_url: r,
967
+ image_url: i || ""
968
+ }, m = N(e), p = u ? u.audio_url_lossless : m && m !== r ? m : void 0;
969
+ p && (d.audio_url_lossless = p), o && (d.selected_mix = o), n && (d.image_thumb_url = n), f && (d.description = f), "audio_ipfs_hash" in e && typeof e.audio_ipfs_hash == "string" && (d.audio_ipfs_hash = e.audio_ipfs_hash), "artwork_ipfs_hash" in e && typeof e.artwork_ipfs_hash == "string" && (d.artwork_ipfs_hash = e.artwork_ipfs_hash), "mint_function" in e && typeof e.mint_function == "string" && (d.mint_function = e.mint_function), "mint_price" in e && typeof e.mint_price == "string" && (d.mint_price = e.mint_price), "mint_snapshot_time" in e && typeof e.mint_snapshot_time == "number" && (d.mint_snapshot_time = e.mint_snapshot_time), "mint_token" in e && typeof e.mint_token == "string" && (d.mint_token = e.mint_token), t.push(d);
970
+ }
971
+ }
972
+ return t;
973
+ }
974
+ function Ne(s) {
975
+ const t = [];
976
+ for (const e of s.tracks) {
977
+ if ("audio_url" in e) {
978
+ t.push({ ...e });
979
+ continue;
980
+ }
981
+ if ("metadata" in e) {
982
+ const r = c.parseMetadata(e.metadata);
983
+ if (r) {
984
+ const i = A(r), n = k(
985
+ e.selected_mix,
986
+ c.convertAttributes(r.attributes),
987
+ i.mixes
988
+ ), u = c.extractAudioUrlLossless(r), o = c.extractAudioUrl(r), a = n?.audio_url || o, l = n ? n.audio_url_lossless : u && u !== a ? u : void 0;
989
+ t.push({
990
+ id: e.chain_name === "AR" ? `AR-${e.tx_id}` : `${e.chain_name}-${e.token_address}-${e.token_id}`,
991
+ chain_name: e.chain_name,
992
+ token_address: e.token_address,
993
+ token_id: e.token_id,
994
+ tx_id: e.tx_id,
995
+ platform: e.platform,
996
+ title: c.extractTitle(r),
997
+ artist: c.extractArtist(r),
998
+ audio_url: a,
999
+ image_url: c.extractImageUrl(r),
1000
+ image_thumb_url: c.extractImageThumbUrl(r),
1001
+ duration: r.duration,
1002
+ genre: r.genre,
1003
+ tags: r.tags,
1004
+ bpm: r.bpm,
1005
+ key: r.key,
1006
+ album: r.album,
1007
+ description: c.extractDescription(r),
1008
+ attributes: c.convertAttributes(r.attributes),
1009
+ ...l ? { audio_url_lossless: l } : {},
1010
+ ...n ? { selected_mix: n.name } : {},
1011
+ ...i.hasExtras ? { extras: i } : {},
1012
+ // v0.4 IPFS hash + mint fields from the raw track wrapper
1013
+ ...e.audio_ipfs_hash ? { audio_ipfs_hash: e.audio_ipfs_hash } : {},
1014
+ ...e.artwork_ipfs_hash ? { artwork_ipfs_hash: e.artwork_ipfs_hash } : {},
1015
+ ...e.mint_function ? { mint_function: e.mint_function } : {},
1016
+ ...e.mint_price ? { mint_price: e.mint_price } : {},
1017
+ ...e.mint_snapshot_time ? { mint_snapshot_time: e.mint_snapshot_time } : {},
1018
+ ...e.mint_token ? { mint_token: e.mint_token } : {}
1019
+ });
1020
+ }
1021
+ }
1022
+ }
1023
+ return t;
1024
+ }
1025
+ function Pe(s) {
1026
+ const t = [];
1027
+ for (const e of s.tracks) {
1028
+ const r = L(e);
1029
+ r && t.push(r);
1030
+ }
1031
+ return t;
1032
+ }
1033
+ function Le(s) {
1034
+ const t = [];
1035
+ for (const e of s.tracks) {
1036
+ const r = L(e);
1037
+ if (!r) continue;
1038
+ const i = h(e), n = _(e), u = P(e);
1039
+ let o = "", a = "", l;
1040
+ if ("title" in e)
1041
+ o = e.title || "", a = e.artist || "", l = e.description || void 0;
1042
+ else if ("metadata" in e) {
1043
+ const d = c.parseMetadata(e.metadata);
1044
+ d && (o = c.extractTitle(d) || "", a = c.extractArtist(d) || "", l = c.extractDescription(d) || void 0);
1045
+ }
1046
+ const f = {
1047
+ title: o,
1048
+ artist: a,
1049
+ audio_url: i || "",
1050
+ image_url: n || "",
1051
+ vrm: r
1052
+ };
1053
+ u && (f.image_thumb_url = u), l && (f.description = l), t.push(f);
1054
+ }
1055
+ return t;
1056
+ }
1057
+ function Ue(s) {
1058
+ const t = [];
1059
+ for (const e of s.tracks)
1060
+ t.push(...U(e));
1061
+ return t;
1062
+ }
1063
+ function Re(s) {
1064
+ const t = [];
1065
+ for (const e of s.tracks) {
1066
+ const r = U(e);
1067
+ if (r.length === 0) continue;
1068
+ const i = R(e);
1069
+ let n = "", u = "", o;
1070
+ if ("title" in e)
1071
+ n = e.title || "", u = e.artist || "", o = e.description || void 0;
1072
+ else if ("metadata" in e) {
1073
+ const d = c.parseMetadata(e.metadata);
1074
+ d && (n = c.extractTitle(d) || "", u = c.extractArtist(d) || "", o = c.extractDescription(d) || void 0);
1075
+ }
1076
+ let a = h(e) || "", l;
1077
+ if (i)
1078
+ a = i.audio_url || a, l = i.audio_url_lossless;
1079
+ else {
1080
+ const d = N(e);
1081
+ l = d && d !== a ? d : void 0;
1082
+ }
1083
+ const f = i ? i.name : "audio_url" in e ? e.selected_mix : void 0;
1084
+ t.push({
1085
+ title: n,
1086
+ artist: u,
1087
+ audio_url: a,
1088
+ image_url: _(e) || "",
1089
+ ...l ? { audio_url_lossless: l } : {},
1090
+ ...f ? { selected_mix: f } : {},
1091
+ ...o ? { description: o } : {},
1092
+ mixes: r
1093
+ });
1094
+ }
1095
+ return t;
1096
+ }
1097
+ const x = [
1098
+ "ipfs.io",
1099
+ "cloudflare-ipfs.com",
1100
+ "dweb.link",
1101
+ "gateway.pinata.cloud",
1102
+ "ipfs.infura.io",
1103
+ "nftstorage.link",
1104
+ "web3-music-pipeline.mypinata.cloud",
1105
+ "soundxyz.mypinata.cloud",
1106
+ "gold-broad-gibbon-29.mypinata.cloud",
1107
+ "bonfire.mypinata.cloud",
1108
+ "spinamp.mypinata.cloud",
1109
+ "catalogworks.b-cdn.net",
1110
+ "www.heds.cloud"
1111
+ ], ae = "ipfs.io", v = "/ipfs/", oe = /^Qm[1-9A-HJ-NP-Za-km-z]{44}$/, le = /^b[a-z2-7]{55,}$/;
1112
+ function ue(s) {
1113
+ return oe.test(s) || le.test(s);
1114
+ }
1115
+ const ce = /^ipfs_[a-z]+$/;
1116
+ function D(s) {
1117
+ const t = s.split("/").filter(Boolean);
1118
+ for (let e = 0; e < t.length - 1; e++)
1119
+ if (ce.test(t[e]) && ue(t[e + 1])) {
1120
+ const r = t[e + 1], i = t.slice(e + 2);
1121
+ return i.length > 0 ? `${r}/${i.join("/")}` : r;
1122
+ }
1123
+ return null;
1124
+ }
1125
+ function de(s) {
1126
+ if (!s || typeof s != "string") return null;
1127
+ if (s.startsWith("ipfs://ipfs/"))
1128
+ return s.slice(12);
1129
+ if (s.startsWith("ipfs://"))
1130
+ return s.slice(7);
1131
+ try {
1132
+ const t = new URL(s), { hostname: e, pathname: r } = t, i = e.match(/^([A-Za-z0-9]+)\.ipfs\.dweb\.link$/);
1133
+ if (i)
1134
+ return `${i[1]}${r}`;
1135
+ if (x.some(
1136
+ (o) => e === o || e.endsWith("." + o)
1137
+ ) && r.startsWith(v))
1138
+ return r.slice(v.length);
1139
+ const u = D(r);
1140
+ if (u) return u;
1141
+ } catch {
1142
+ }
1143
+ return null;
1144
+ }
1145
+ function De(s) {
1146
+ if (!s || typeof s != "string") return !1;
1147
+ if (s.startsWith("ipfs://")) return !0;
1148
+ try {
1149
+ const t = new URL(s), { hostname: e, pathname: r } = t;
1150
+ if (/^[A-Za-z0-9]+\.ipfs\.dweb\.link$/.test(e) || x.some(
1151
+ (n) => e === n || e.endsWith("." + n)
1152
+ ) && r.startsWith(v) || D(r)) return !0;
1153
+ } catch {
1154
+ }
1155
+ return !1;
1156
+ }
1157
+ function fe(s) {
1158
+ if (!(!s || s.startsWith("ipfs://")))
1159
+ try {
1160
+ const { hostname: t } = new URL(s);
1161
+ return x.some(
1162
+ (r) => t === r || t.endsWith("." + r)
1163
+ ) ? t : void 0;
1164
+ } catch {
1165
+ return;
1166
+ }
1167
+ }
1168
+ function Oe(s, t = {}) {
1169
+ if (!s) return s;
1170
+ const e = typeof t == "string" ? { gatewayHost: t } : t, { gatewayHost: r = ae, preserveOriginalGateway: i = !1 } = e, n = de(s);
1171
+ if (n) {
1172
+ if (i) {
1173
+ const u = fe(s);
1174
+ if (u)
1175
+ return `https://${u}/ipfs/${n}`;
1176
+ }
1177
+ return `https://${r}/ipfs/${n}`;
1178
+ }
1179
+ return s;
1180
+ }
1181
+ const me = "---\nname: playlist-data-engine\ndescription: The playlist-data-engine npm package — parse Arweave/IPFS serverless music playlists (Ethereum NFTs + Arweave uploads; the data behind ar://listen and ApeTapes) into clean track data; resolve alternate mixes (entry pins, selected_mix, alias-tolerant lookup, lossy/lossless pairs) and track extras/stems; reach files through Arweave gateway failover (AR.IO Wayfinder); analyze audio (sonic fingerprint, genre/mood classification, pitch, BPM/beat maps, rhythm charts) and drive deterministic D&D-style generation from the music — characters, enemies, combat, equipment, XP. Use when working with the playlist-data-engine package, serverless playlist JSON, Arweave music NFTs, or building music-reactive or rhythm-game features on this engine.\n---\n\n# playlist-data-engine\n\nOne engine, one pipeline: a **serverless playlist** (Arweave JSON mixing Ethereum NFTs and Arweave uploads) is parsed into clean tracks; track audio is analyzed; analysis drives **deterministic generation** — characters, enemies, rhythm charts, combat; XP/progression and IRL sensors make the result a living music player. Two halves meet at audio: the data half (parse → extract → resolve → analyze) and the game half (beat map → chart → character/XP/combat).\n\n## First moves\n\n- Docs ship **inside the package**, version-matched to your install: `node_modules/playlist-data-engine/docs/`. If docs and observed behavior disagree, check your installed version.\n- `docs/DATA_ENGINE_REFERENCE.md` is the master index — every export, one row each, **no examples or algorithms by design** (those live in USAGE and features/*). Start at its **Quick Export Reference** to find the function, then jump to its section. Caveat: its Core table predates the entry-point split — the ML classes listed there now live behind `/analysis`.\n- `docs/USAGE_IN_OTHER_PROJECTS.md` has working code for every major feature (install, full pipeline, troubleshooting).\n- CLI: `npx playlist-data-engine docs` (doc index) · `docs <topic>` (full text) · `skill` (this file). From code: `engineHelp()` / `engineHelp('<topic>')` / `engineHelp('skill')` — exported from both the default and `/gateway` entries. Canonical copy of this file: `skills/playlist-data-engine/SKILL.md` in the repo.\n- The `.d.ts` in `dist/` is the complete API surface with JSDoc.\n\n## Three entry points — pick by bundle weight\n\n| Import | Gives you | Pulls TF? |\n|---|---|---|\n| `playlist-data-engine` (default) | Everything **except** ML — parser, playlists, mixes, gateway, generation, combat, XP, sensors, beat detection | No |\n| `playlist-data-engine/gateway` | Lightest: `ArweaveGatewayManager` + Arweave/IPFS URL utils, `MetadataExtractor`, mix lookup (`findMixByName`/`resolveMixUrl`/`selectMix`), `getMixes`/`getMixTracks`/VRM getters | No |\n| `playlist-data-engine/analysis` | TensorFlow-bearing: `AudioAnalyzer`, `MusicClassifier`, `PitchAnalyzer`, `EssentiaPitchDetector`, `PitchBeatLinker`, `ButtonMapper`, `LevelGenerator`, `LevelSerializer`, `BeatConverter`, `ModelCache` | Yes (~14 MB) |\n\nRules: default to the bare import; import `/analysis` only in code that actually runs ML — ideally isolated in a Web Worker; never re-export `/analysis` symbols through the other entries.\n\n## The map\n\n| You want… | Reach for | Read |\n|---|---|---|\n| Parse a playlist | `new PlaylistParser().parse(raw)` — takes any raw object; options `strict`, `validateAudioUrls`, `resolveImageUrls`. Gateway fetch is yours: `arweaveGatewayManager.resolveUrl()` + `fetch` | features/PLAYLIST_PARSING.md |\n| Quick data arrays | `getAudioUrls`, `getTrackTitles`, `getArtists`, `getGenres`, `getTags`, `getTotalDuration`, `getTracks`, `getFullTracks` — work on raw **and** parsed input | DATA_ENGINE_REFERENCE.md → Playlist Utilities |\n| Track extras (stems, mixes, VRM, lyrics, charts…) | `track.extras`, `getTrackMetadata(raw)`, `getTrackExtras(metadata)` | features/PLAYLIST_PARSING.md |\n| Alternate mixes | See **Mixes** below | features/PLAYLIST_PARSING.md |\n| Conditional mixes (weather/time/plays) | `evaluateMixConditions(extras, { environment, appState })` — unknown condition types always pass; `weather` conditions need `WEATHER_API_KEY`, day/time pass keyless | features/PLAYLIST_PARSING.md |\n| Resolve any Arweave URL | `arweaveGatewayManager.resolveUrl(url)` — cache → the gateway already in the URL → persisted gateway → arweave.net → static gateways in parallel → AR.IO Wayfinder last. Hot path: `resolveUrlSimple()`. After a real fetch fails: `reportGatewayFailure(url, { reason })`. Non-Arweave URLs pass through | features/GATEWAY_RESOLUTION.md |\n| IPFS URLs | `isIPFS`, `extractIPFSPath`, `resolveIPFSLink` | features/GATEWAY_RESOLUTION.md |\n| Sonic fingerprint / full timeline | `AudioAnalyzer.extractSonicFingerprint(url)` (samples 5%/40%/70%), `.analyzeTimeline(url, { type: 'interval' \\| 'count', … })` | features/AUDIO_ANALYSIS.md |\n| Genre / mood / vibes | `MusicClassifier.analyze(url)` — 400+ subgenres, mood themes, danceability/energy/valence; zero-config defaults load Arweave-hosted models; `preset:` swaps models | features/AUDIO_ANALYSIS.md |\n| Pitch / melody | `PitchDetector` (pure pYIN, TF-free, **default entry**) or `/analysis` `PitchAnalyzer.analyze(url)` (adds contour; `EssentiaPitchDetector` needs `await .create()`) | features/AUDIO_ANALYSIS.md |\n| Colors from artwork | `ColorExtractor.extractPalette(imageUrl)` (default entry, no TF) | DATA_ENGINE_REFERENCE.md → ColorExtractor |\n| Detect beats / play in rhythm | `BeatMapGenerator.generateBeatMap()` → `BeatStream` (real-time sync, `checkButtonPress`); `GrooveAnalyzer` (DMC-style groove meter); interpolation/subdivision helpers; manual chart editing via `reapplyDownbeatConfig` + beat key helpers | features/BEAT_DETECTION.md |\n| Procedural rhythm charts | `RhythmGenerator` (default entry: transients → quantize → phrases → composite streams → difficulty variants) or `/analysis` `LevelGenerator` (adds pitch-driven `ButtonMapper` for DDR/Guitar Hero/Tap) | features/BEAT_DETECTION.md |\n| Character from a song | `CharacterGenerator.generate(seed, audioProfile, track, { gameMode: 'standard' \\| 'uncapped' })` — same song, same character. `audioProfile` = `/analysis` `AudioAnalyzer.extractSonicFingerprint()` — or any plain `AudioProfile` object (TF-free synthesis is valid) | USAGE_IN_OTHER_PROJECTS.md |\n| Enemies / encounters | `EnemyGenerator.generate()` / `generateEncounter(party, opts)` / `generateEncounterByCR(opts)` — **CR = power, rarity = complexity, independent axes**; audio profile steers templates & stats | features/ENEMY_GENERATION.md |\n| Combat & balance | `CombatEngine` (turn-based; `hitMode: 'scaled'` default — AC reduces damage — vs `'dnd'`), `CombatSimulator` (Monte Carlo, per-run seeds), `CombatAI`/`AICombatRunner`, `DifficultyCalculator`, `BalanceValidator`, `ParameterSweep` | features/COMBAT_SYSTEM.md |\n| Dice & seeds | `generateSeed(chain, address, tokenId)`, `deriveSeed`, `SeededRNG`, `DiceRoller`, `SeededDiceRoller`/`createSeededRoller` | features/ROLLS_AND_SEEDS.md |\n| Equipment / loot | `EquipmentGenerator`, `EquipmentModifier` (enchant/curse/upgrade), `EquipmentSpawnHelper` (batch spawn), `BoxOpener` (box items) | features/EQUIPMENT_SYSTEM.md |\n| XP & leveling | `SessionTracker` → `CharacterUpdater.updateCharacterFromSession()` (~1 XP/s × modifiers), `addXP`/`addRhythmXP` for other sources; `StatManager` strategies; `PrestigeSystem` (track mastery); uncapped XP curves | features/XP_AND_STATS.md |\n| IRL context | `EnvironmentalSensors` (GPS/motion/weather; solar math needs no API key), `GamingPlatformSensors` (Steam); XP modifier capped at 3.0× | features/IRL_SENSORS.md |\n| Custom content | `ExtensionManager.getInstance().register(category, items, { mode, weights })` — races, classes, spells, skills, `classFeatures`, `racialTraits`, equipment; runtime only | features/EXTENSIBILITY_GUIDE.md (+ CUSTOM_CONTENT.md, CONTENT_PACKS.md, PREREQUISITES.md) |\n| Something broke | ML models load from Arweave at runtime (offline dev fails); audio analysis needs Web Audio | USAGE_IN_OTHER_PROJECTS.md → Troubleshooting |\n| Validate data | Zod schemas: `ServerlessPlaylistSchema`, `PlaylistTrackSchema`, `CharacterSheetSchema`, `AudioProfileSchema`, `MixInfoSchema`, … | DATA_ENGINE_REFERENCE.md → Utilities |\n\n## Mixes — the subtle part\n\n- A playlist **entry** pins a mix via `selected_mix` on the track wrapper. Pin matching is **exact and case-sensitive** — `'default'`, or a name matching nothing, means unpinned; legacy `Selected Mix` attribute is the fallback. A pinned track's `audio_url` (and `audio_url_lossless`) are repointed at parse time, so it just plays. `getTracks`/`getFullTracks` apply pins on the raw path too.\n- **User choice after parse** is the tolerant path over `track.extras.mixes`: `findMixByName(mixes, name, { prefer, aliases, caseInsensitive })` and `selectMix(track, name)` — case-insensitive, trimmed, alias-aware, exact names always win. `'tv mix'` finds Karaoke, which is its own concept, **never** an instrumental. One name can ship twice (lossy + lossless master): `prefer` defaults to `'lossy'`; `getUniqueMixes`/`getPreferredMixByQuality` handle grouping. Pass `conditionsContext` to `selectMix` to enforce a gated mix's conditions at choice time.\n- Audio/mix URIs are **not** resolved at parse time (only images, and only with `resolveImageUrls: true`). `resolveMixUrl(mix)` gateway-resolves for you and follows metadata-JSON uris (`application/json`) one hop to the real audio file.\n\n## Cross-cutting rules\n\n- **Determinism is the contract.** `generateSeed`/`deriveSeed` → `SeededRNG` (generation) and `SeededDiceRoller` (combat dice): same seed + same inputs = byte-identical output. This is why characters are cacheable per track id.\n- **Leveling config:** `LevelUpProcessor.setUncappedConfig()` is genuinely global (set before leveling uncapped characters). XP math is otherwise **per-instance**: `new XPCalculator({ xp_per_second, activity_bonuses })`, optionally handed to `new SessionTracker(calculator)` — `mergeProgressionConfig()` is a stateless merge helper, not global state.\n- **Extensions are runtime-only.** Nothing persists across restarts; re-register on boot or round-trip `exportCustomData()`.\n- **No `src` ships in the package** — consumers get built `dist` (plus `docs/`, `bin/`, `skills/`, `llms.txt`). Consumers on `file:` deps read built dist — rebuild the engine (`npm run build`) after source changes, then restart the consumer's dev server (webpack does not invalidate `file:` symlink targets).\n- **Environment:** audio analysis + beat detection need Web Audio (browser, or the `web-audio-api` polyfill in Node); the gateway manager persists state to `localStorage` (shim it server-side); ML models fetch from Arweave at runtime.\n- **Sensors are optional.** Env keys: `WEATHER_API_KEY`, `STEAM_API_KEY`, `STEAM_USER_ID`, `XP_MAX_MODIFIER`; sunrise/sunset math needs no key.\n", O = [
1182
+ { slug: "data_engine_reference", file: "docs/DATA_ENGINE_REFERENCE.md", summary: "Every public function in one table — signatures, inputs, outputs." },
1183
+ { slug: "usage_in_other_projects", file: "docs/USAGE_IN_OTHER_PROJECTS.md", summary: "Install, main vs gateway entry, first parsed playlist." },
1184
+ { slug: "playlist_parsing", file: "docs/features/PLAYLIST_PARSING.md", summary: "Loading playlists, track objects, alternate mixes: pins, alias matching, lossy/lossless pairs." },
1185
+ { slug: "gateway_resolution", file: "docs/features/GATEWAY_RESOLUTION.md", summary: "Gateway failover, Wayfinder, resolving tx and IPFS URLs." },
1186
+ { slug: "audio_analysis", file: "docs/features/AUDIO_ANALYSIS.md", summary: "BPM, key, mood, genre classification (TensorFlow + essentia)." },
1187
+ { slug: "beat_detection", file: "docs/features/BEAT_DETECTION.md", summary: "Beat streams and rhythm-game chart generation." },
1188
+ { slug: "combat_system", file: "docs/features/COMBAT_SYSTEM.md", summary: "Seeded combat simulations driven by track energy." },
1189
+ { slug: "content_packs", file: "docs/features/CONTENT_PACKS.md", summary: "Bundled content collections." },
1190
+ { slug: "custom_content", file: "docs/features/CUSTOM_CONTENT.md", summary: "Authoring your own content." },
1191
+ { slug: "enemy_generation", file: "docs/features/ENEMY_GENERATION.md", summary: "Characters generated from sonic fingerprints." },
1192
+ { slug: "equipment_system", file: "docs/features/EQUIPMENT_SYSTEM.md", summary: "Loot and equipment." },
1193
+ { slug: "extensibility_guide", file: "docs/features/EXTENSIBILITY_GUIDE.md", summary: "Extending the engine." },
1194
+ { slug: "irl_sensors", file: "docs/features/IRL_SENSORS.md", summary: "Weather, time, motion context for condition-gated mixes." },
1195
+ { slug: "prerequisites", file: "docs/features/PREREQUISITES.md", summary: "Required setup." },
1196
+ { slug: "rolls_and_seeds", file: "docs/features/ROLLS_AND_SEEDS.md", summary: "Deterministic seeded dice." },
1197
+ { slug: "xp_and_stats", file: "docs/features/XP_AND_STATS.md", summary: "Progression and persistence." }
1198
+ ], G = {
1199
+ reference: "data_engine_reference",
1200
+ usage: "usage_in_other_projects",
1201
+ parsing: "playlist_parsing",
1202
+ mixes: "playlist_parsing",
1203
+ gateway: "gateway_resolution",
1204
+ docs: "data_engine_reference"
1205
+ }, y = [
1206
+ {
1207
+ version: "1.7.3",
1208
+ uploadedAt: "2026-10-05T00:00:00.000Z",
1209
+ files: {
1210
+ "DATA_ENGINE_REFERENCE.md": "TWQ0ashXikM82N2fEmeSSWtV72CavF6kySmZWWyTUX0",
1211
+ "USAGE_IN_OTHER_PROJECTS.md": "wzeOtdQnuAx4Tk_SuTL0Mzkk05EviFljJdpI93pB7Ek",
1212
+ "features/AUDIO_ANALYSIS.md": "v6apJcfWhSXSNf2zFgl36xaeurTsfm89YUvi0PPGUbs",
1213
+ "features/BEAT_DETECTION.md": "IK26T_JcCnS5SVWmvaSzLL_QyihvyCpKXaUrBlhQRow",
1214
+ "features/COMBAT_SYSTEM.md": "dk8G9qnh76tpjudjOhzvCEL89Wm2ZNHY_BPqIzuoxLg",
1215
+ "features/CONTENT_PACKS.md": "QzoLMibxAWtJ6NfM82f_xZUh4tLAoF0ZoQf-vzI-r5c",
1216
+ "features/CUSTOM_CONTENT.md": "76hpg3ox37NfaK4b1PplPrJpAP3PXBqQwTtpOhvFWXo",
1217
+ "features/ENEMY_GENERATION.md": "eeEuV9bCP8jxX5_DU6V6p2imbiJ251cTM8dM3UqaCbc",
1218
+ "features/EQUIPMENT_SYSTEM.md": "I8GH9d6UuHl1bbdLAN5LUo4ppcPu5cK320q_HQ0GEzU",
1219
+ "features/EXTENSIBILITY_GUIDE.md": "8ha92HARwcA1AQ0z5OqmdXWa7VPDcPrImqF1MB2Dd5Q",
1220
+ "features/GATEWAY_RESOLUTION.md": "__oqyaVqA-DPF_M_9_C9S8nYNohXjJd2b2YIU9uYVHE",
1221
+ "features/IRL_SENSORS.md": "AodJ11PvwSNnT8hoReib4lWArlfO6nH0tU3Gz5x5ul8",
1222
+ "features/PLAYLIST_PARSING.md": "CDZOVCe1RTVWQKhG-FMj12XA0hAb6E_Gj-UBq1-h9SY",
1223
+ "features/PREREQUISITES.md": "CnP5rxjK_5xX70-1SiCzymVROaARp5Um2yESF2yaTmQ",
1224
+ "features/ROLLS_AND_SEEDS.md": "y1mCkus6jre7h_jHDZMM_PGoucrDuUazNBnCZD2-zdo",
1225
+ "features/XP_AND_STATS.md": "UV4cFhuL7Un8XuguyAKNu0Qo1RlIiKYGWiBdvBJzEEM"
1226
+ }
1227
+ }
1228
+ ], pe = "https://github.com/jasondesante/playlist-data-engine/blob/main";
1229
+ function F() {
1230
+ const s = O.map((r) => `- \`${r.slug}\` — ${r.file}: ${r.summary}`).join(`
1231
+ `), t = y[y.length - 1];
1232
+ return [
1233
+ "# playlist-data-engine — docs index",
1234
+ "",
1235
+ "Parse Arweave serverless music playlists, resolve alternate mixes and quality, reach files through gateway failover.",
1236
+ "",
1237
+ "## Where the full docs are",
1238
+ "",
1239
+ "- In this install: `node_modules/playlist-data-engine/docs/` (version-matched — prefer this)",
1240
+ "- CLI: `npx playlist-data-engine docs <topic>`",
1241
+ "- GitHub: https://github.com/jasondesante/playlist-data-engine/tree/main/docs",
1242
+ ...t ? [`- Permanent archive on Arweave: latest snapshot is v${t.version} (${t.uploadedAt}) — every doc also has an \`ar://\` link via \`engineHelp('<topic>')\``, "- All archived versions stay addressable forever: " + y.map((r) => `v${r.version} (${r.uploadedAt.slice(0, 10)})`).join(", ")] : [],
1243
+ "",
1244
+ "## Topics",
1245
+ "",
1246
+ s,
1247
+ "",
1248
+ "## For AI agents",
1249
+ "",
1250
+ "- `engineHelp('<topic>')` returns where to read one doc (aliases: " + Object.keys(G).join(", ") + ")",
1251
+ "- `engineHelp('skill')` returns the Claude Code skill file — write it to `.claude/skills/playlist-data-engine/SKILL.md` to install",
1252
+ "- The `.d.ts` in dist/ is the complete API surface with JSDoc",
1253
+ ""
1254
+ ].join(`
1255
+ `);
1256
+ }
1257
+ function g(s) {
1258
+ const t = s.replace(/^docs\//, "");
1259
+ for (let e = y.length - 1; e >= 0; e--) {
1260
+ const r = y[e].files[t];
1261
+ if (r) return { entry: y[e], txId: r };
1262
+ }
1263
+ }
1264
+ function ye(s) {
1265
+ const t = G[s.toLowerCase()] ?? s.toLowerCase(), e = O.find((r) => r.slug === t || r.slug.startsWith(t));
1266
+ return e ? [
1267
+ `# ${e.slug}`,
1268
+ "",
1269
+ e.summary,
1270
+ "",
1271
+ `- File: \`${e.file}\` (relative to the package root)`,
1272
+ `- In this install: \`node_modules/playlist-data-engine/${e.file}\``,
1273
+ `- CLI: \`npx playlist-data-engine docs ${e.slug}\``,
1274
+ `- Online: ${pe}/${e.file}`,
1275
+ ...g(e.file) ? [`- Permanent: ar://${g(e.file).txId} (docs v${g(e.file).entry.version}, uploaded ${g(e.file).entry.uploadedAt.slice(0, 10)})`] : [],
1276
+ "",
1277
+ "Read the file itself for the full text — this pointer is deliberately small so it stays safe in browser bundles.",
1278
+ ""
1279
+ ].join(`
1280
+ `) : `No doc topic "${s}".
1281
+
1282
+ ${F()}`;
1283
+ }
1284
+ function Ge(s) {
1285
+ return s ? s.toLowerCase() === "skill" ? me : ye(s) : F();
1286
+ }
1287
+ export {
1288
+ b as A,
1289
+ xe as B,
1290
+ ae as D,
1291
+ y as E,
1292
+ x as K,
1293
+ c as M,
1294
+ O as a,
1295
+ re as b,
1296
+ de as c,
1297
+ Te as d,
1298
+ Ge as e,
1299
+ ve as f,
1300
+ A as g,
1301
+ be as h,
1302
+ Ne as i,
1303
+ we as j,
1304
+ Ee as k,
1305
+ Re as l,
1306
+ Ue as m,
1307
+ I as n,
1308
+ ke as o,
1309
+ Ie as p,
1310
+ Me as q,
1311
+ k as r,
1312
+ _e as s,
1313
+ Se as t,
1314
+ Ce as u,
1315
+ Ae as v,
1316
+ Le as w,
1317
+ Pe as x,
1318
+ De as y,
1319
+ Oe as z
1320
+ };