trackparse 0.1.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.
- package/README.md +211 -0
- package/dist/index.cjs +2269 -0
- package/dist/index.d.cts +104 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +2261 -0
- package/package.json +73 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2269 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/data.generated.ts
|
|
4
|
+
var SPEC_VERSION = "0.1.0";
|
|
5
|
+
var descriptorsData = {
|
|
6
|
+
"descriptors": [
|
|
7
|
+
"radio",
|
|
8
|
+
"extended",
|
|
9
|
+
"club",
|
|
10
|
+
"original",
|
|
11
|
+
"instrumental",
|
|
12
|
+
"dub",
|
|
13
|
+
"vip",
|
|
14
|
+
"acoustic",
|
|
15
|
+
"live",
|
|
16
|
+
"demo",
|
|
17
|
+
"vocal",
|
|
18
|
+
"clean",
|
|
19
|
+
"dirty",
|
|
20
|
+
"explicit",
|
|
21
|
+
"short",
|
|
22
|
+
"long",
|
|
23
|
+
"full",
|
|
24
|
+
"album",
|
|
25
|
+
"single",
|
|
26
|
+
"alternate",
|
|
27
|
+
"alt",
|
|
28
|
+
"alternative",
|
|
29
|
+
"main",
|
|
30
|
+
"special",
|
|
31
|
+
"bonus",
|
|
32
|
+
"deluxe",
|
|
33
|
+
"mono",
|
|
34
|
+
"stereo",
|
|
35
|
+
"new",
|
|
36
|
+
"official",
|
|
37
|
+
"orchestral",
|
|
38
|
+
"piano",
|
|
39
|
+
"unplugged",
|
|
40
|
+
'12"',
|
|
41
|
+
'7"',
|
|
42
|
+
"12''",
|
|
43
|
+
"7''",
|
|
44
|
+
"12 inch",
|
|
45
|
+
"7 inch",
|
|
46
|
+
"re",
|
|
47
|
+
"final",
|
|
48
|
+
"second",
|
|
49
|
+
"2nd",
|
|
50
|
+
"3rd"
|
|
51
|
+
]
|
|
52
|
+
};
|
|
53
|
+
var extensionsData = {
|
|
54
|
+
"extensions": [
|
|
55
|
+
"mp3",
|
|
56
|
+
"flac",
|
|
57
|
+
"wav",
|
|
58
|
+
"m4a",
|
|
59
|
+
"ogg",
|
|
60
|
+
"opus",
|
|
61
|
+
"aac",
|
|
62
|
+
"aiff",
|
|
63
|
+
"aif",
|
|
64
|
+
"wma",
|
|
65
|
+
"alac",
|
|
66
|
+
"mp4",
|
|
67
|
+
"webm",
|
|
68
|
+
"mkv"
|
|
69
|
+
]
|
|
70
|
+
};
|
|
71
|
+
var featMarkersData = {
|
|
72
|
+
"markers": [
|
|
73
|
+
"feat.",
|
|
74
|
+
"feat",
|
|
75
|
+
"ft.",
|
|
76
|
+
"ft",
|
|
77
|
+
"featuring",
|
|
78
|
+
"feat:",
|
|
79
|
+
"ft:"
|
|
80
|
+
],
|
|
81
|
+
"bracketOnly": [
|
|
82
|
+
"with",
|
|
83
|
+
"w/"
|
|
84
|
+
],
|
|
85
|
+
"producer": [
|
|
86
|
+
"prod.",
|
|
87
|
+
"prod",
|
|
88
|
+
"produced",
|
|
89
|
+
"prod.by",
|
|
90
|
+
"prod:"
|
|
91
|
+
],
|
|
92
|
+
"titleMarkers": [
|
|
93
|
+
"feat.",
|
|
94
|
+
"ft.",
|
|
95
|
+
"featuring"
|
|
96
|
+
],
|
|
97
|
+
"titleProducer": [
|
|
98
|
+
"prod.",
|
|
99
|
+
"prod. by",
|
|
100
|
+
"produced by",
|
|
101
|
+
"prod.by"
|
|
102
|
+
]};
|
|
103
|
+
var genresData = {
|
|
104
|
+
"genres": [
|
|
105
|
+
"d'n'b",
|
|
106
|
+
"dnb",
|
|
107
|
+
"d&b",
|
|
108
|
+
"d & b",
|
|
109
|
+
"drum & bass",
|
|
110
|
+
"drum&bass",
|
|
111
|
+
"drum and bass",
|
|
112
|
+
"drum n bass",
|
|
113
|
+
"drum 'n' bass",
|
|
114
|
+
"drum'n'bass",
|
|
115
|
+
"neurofunk",
|
|
116
|
+
"neuro",
|
|
117
|
+
"liquid",
|
|
118
|
+
"liquid funk",
|
|
119
|
+
"jump up",
|
|
120
|
+
"jump-up",
|
|
121
|
+
"jungle",
|
|
122
|
+
"halftime",
|
|
123
|
+
"dancefloor",
|
|
124
|
+
"dubstep",
|
|
125
|
+
"riddim",
|
|
126
|
+
"brostep",
|
|
127
|
+
"trap",
|
|
128
|
+
"future bass",
|
|
129
|
+
"house",
|
|
130
|
+
"deep house",
|
|
131
|
+
"tech house",
|
|
132
|
+
"progressive house",
|
|
133
|
+
"electro house",
|
|
134
|
+
"techno",
|
|
135
|
+
"trance",
|
|
136
|
+
"psytrance",
|
|
137
|
+
"psy",
|
|
138
|
+
"hardstyle",
|
|
139
|
+
"hardcore",
|
|
140
|
+
"breaks",
|
|
141
|
+
"breakbeat",
|
|
142
|
+
"garage",
|
|
143
|
+
"uk garage",
|
|
144
|
+
"ukg",
|
|
145
|
+
"2step",
|
|
146
|
+
"2-step",
|
|
147
|
+
"grime",
|
|
148
|
+
"bass",
|
|
149
|
+
"bass house",
|
|
150
|
+
"edm",
|
|
151
|
+
"electronic",
|
|
152
|
+
"idm",
|
|
153
|
+
"synthwave",
|
|
154
|
+
"phonk",
|
|
155
|
+
"hip hop",
|
|
156
|
+
"hip-hop",
|
|
157
|
+
"rap",
|
|
158
|
+
"pop",
|
|
159
|
+
"rock",
|
|
160
|
+
"metal",
|
|
161
|
+
"ambient",
|
|
162
|
+
"chillout",
|
|
163
|
+
"lofi",
|
|
164
|
+
"lo-fi"
|
|
165
|
+
]
|
|
166
|
+
};
|
|
167
|
+
var joinersData = {
|
|
168
|
+
"spaced": [
|
|
169
|
+
{
|
|
170
|
+
"raw": "&",
|
|
171
|
+
"canonical": "&"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"raw": "and",
|
|
175
|
+
"canonical": "&",
|
|
176
|
+
"kind": "and"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"raw": "x",
|
|
180
|
+
"canonical": "x",
|
|
181
|
+
"caseSensitive": true
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"raw": "\xD7",
|
|
185
|
+
"canonical": "x"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"raw": "vs.",
|
|
189
|
+
"canonical": "vs."
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"raw": "vs",
|
|
193
|
+
"canonical": "vs."
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"raw": "versus",
|
|
197
|
+
"canonical": "vs."
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"raw": "/",
|
|
201
|
+
"canonical": "/"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"raw": "+",
|
|
205
|
+
"canonical": "+"
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"raw": "with",
|
|
209
|
+
"canonical": "with"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"raw": "meets",
|
|
213
|
+
"canonical": "meets"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"raw": "pres.",
|
|
217
|
+
"canonical": "pres."
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"raw": "presents",
|
|
221
|
+
"canonical": "pres."
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"raw": "b2b",
|
|
225
|
+
"canonical": "b2b"
|
|
226
|
+
}
|
|
227
|
+
],
|
|
228
|
+
"tight": [
|
|
229
|
+
{
|
|
230
|
+
"raw": ",",
|
|
231
|
+
"canonical": ","
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"raw": ";",
|
|
235
|
+
"canonical": ","
|
|
236
|
+
}
|
|
237
|
+
],
|
|
238
|
+
"featCanonical": "feat."
|
|
239
|
+
};
|
|
240
|
+
var junkData = {
|
|
241
|
+
"phrases": {
|
|
242
|
+
"official video": "video",
|
|
243
|
+
"official music video": "video",
|
|
244
|
+
"music video": "video",
|
|
245
|
+
"official video clip": "video",
|
|
246
|
+
"video clip": "video",
|
|
247
|
+
"official clip": "video",
|
|
248
|
+
"clip officiel": "video",
|
|
249
|
+
"official hd video": "video",
|
|
250
|
+
"official 4k video": "video",
|
|
251
|
+
"official mv": "video",
|
|
252
|
+
"mv": "video",
|
|
253
|
+
"m/v": "video",
|
|
254
|
+
"video": "video",
|
|
255
|
+
"official visualizer": "video",
|
|
256
|
+
"official visualiser": "video",
|
|
257
|
+
"visualizer": "video",
|
|
258
|
+
"visualiser": "video",
|
|
259
|
+
"audio visualizer": "video",
|
|
260
|
+
"official animated video": "video",
|
|
261
|
+
"animated video": "video",
|
|
262
|
+
"live video": "video",
|
|
263
|
+
"official live video": "video",
|
|
264
|
+
"performance video": "video",
|
|
265
|
+
"official performance video": "video",
|
|
266
|
+
"official audio": "audio",
|
|
267
|
+
"audio": "audio",
|
|
268
|
+
"audio only": "audio",
|
|
269
|
+
"full audio": "audio",
|
|
270
|
+
"official lyric video": "lyrics",
|
|
271
|
+
"official lyrics video": "lyrics",
|
|
272
|
+
"lyric video": "lyrics",
|
|
273
|
+
"lyrics video": "lyrics",
|
|
274
|
+
"lyrics": "lyrics",
|
|
275
|
+
"lyric": "lyrics",
|
|
276
|
+
"with lyrics": "lyrics",
|
|
277
|
+
"lyrics on screen": "lyrics",
|
|
278
|
+
"letra": "lyrics",
|
|
279
|
+
"hd": "quality",
|
|
280
|
+
"hq": "quality",
|
|
281
|
+
"uhd": "quality",
|
|
282
|
+
"4k": "quality",
|
|
283
|
+
"8k": "quality",
|
|
284
|
+
"1080p": "quality",
|
|
285
|
+
"720p": "quality",
|
|
286
|
+
"480p": "quality",
|
|
287
|
+
"high quality": "quality",
|
|
288
|
+
"full hd": "quality",
|
|
289
|
+
"free download": "promo",
|
|
290
|
+
"free dl": "promo",
|
|
291
|
+
"out now": "promo",
|
|
292
|
+
"premiere": "promo",
|
|
293
|
+
"exclusive": "promo",
|
|
294
|
+
"preview": "promo",
|
|
295
|
+
"teaser": "promo",
|
|
296
|
+
"snippet": "promo",
|
|
297
|
+
"new song": "promo",
|
|
298
|
+
"available now": "promo",
|
|
299
|
+
"ncs release": "label",
|
|
300
|
+
"monstercat release": "label",
|
|
301
|
+
"official": "other"
|
|
302
|
+
},
|
|
303
|
+
"connectors": [
|
|
304
|
+
"-",
|
|
305
|
+
"|",
|
|
306
|
+
"/",
|
|
307
|
+
"&",
|
|
308
|
+
"+",
|
|
309
|
+
","
|
|
310
|
+
],
|
|
311
|
+
"labelSuffixes": [
|
|
312
|
+
"release",
|
|
313
|
+
"records",
|
|
314
|
+
"recordings"
|
|
315
|
+
]
|
|
316
|
+
};
|
|
317
|
+
var noSplitBeforeData = {
|
|
318
|
+
"words": [
|
|
319
|
+
"sons",
|
|
320
|
+
"daughters",
|
|
321
|
+
"friends",
|
|
322
|
+
"co",
|
|
323
|
+
"co.",
|
|
324
|
+
"company",
|
|
325
|
+
"his",
|
|
326
|
+
"her",
|
|
327
|
+
"their",
|
|
328
|
+
"orchestra"
|
|
329
|
+
]
|
|
330
|
+
};
|
|
331
|
+
var platformSuffixesData = {
|
|
332
|
+
"suffixes": [
|
|
333
|
+
" - YouTube Music",
|
|
334
|
+
" - YouTube",
|
|
335
|
+
" | Free Listening on SoundCloud",
|
|
336
|
+
" - SoundCloud",
|
|
337
|
+
" | SoundCloud",
|
|
338
|
+
" - Topic",
|
|
339
|
+
" | Bandcamp",
|
|
340
|
+
" - Bandcamp"
|
|
341
|
+
]
|
|
342
|
+
};
|
|
343
|
+
var stopwordsData = {
|
|
344
|
+
"words": [
|
|
345
|
+
"you",
|
|
346
|
+
"me",
|
|
347
|
+
"him",
|
|
348
|
+
"her",
|
|
349
|
+
"us",
|
|
350
|
+
"them",
|
|
351
|
+
"it",
|
|
352
|
+
"i",
|
|
353
|
+
"love",
|
|
354
|
+
"my",
|
|
355
|
+
"your",
|
|
356
|
+
"our",
|
|
357
|
+
"the",
|
|
358
|
+
"a",
|
|
359
|
+
"an",
|
|
360
|
+
"this",
|
|
361
|
+
"that",
|
|
362
|
+
"myself",
|
|
363
|
+
"yourself",
|
|
364
|
+
"everyone",
|
|
365
|
+
"everybody",
|
|
366
|
+
"somebody",
|
|
367
|
+
"someone",
|
|
368
|
+
"nobody",
|
|
369
|
+
"nothing",
|
|
370
|
+
"all",
|
|
371
|
+
"night",
|
|
372
|
+
"side",
|
|
373
|
+
"day"
|
|
374
|
+
]
|
|
375
|
+
};
|
|
376
|
+
var unknownTokensData = {
|
|
377
|
+
"caseSensitive": [
|
|
378
|
+
"ID",
|
|
379
|
+
"IDs",
|
|
380
|
+
"ID?"
|
|
381
|
+
],
|
|
382
|
+
"caseInsensitive": [
|
|
383
|
+
"unknown",
|
|
384
|
+
"unknown artist",
|
|
385
|
+
"unknown title",
|
|
386
|
+
"untitled",
|
|
387
|
+
"?",
|
|
388
|
+
"??",
|
|
389
|
+
"???"
|
|
390
|
+
]
|
|
391
|
+
};
|
|
392
|
+
var versionKeywordsData = {
|
|
393
|
+
"heads": {
|
|
394
|
+
"remix": "remix",
|
|
395
|
+
"rmx": "remix",
|
|
396
|
+
"mix": "mix",
|
|
397
|
+
"bootleg": "bootleg",
|
|
398
|
+
"vip": "vip",
|
|
399
|
+
"edit": "edit",
|
|
400
|
+
"re-edit": "edit",
|
|
401
|
+
"reedit": "edit",
|
|
402
|
+
"flip": "flip",
|
|
403
|
+
"refix": "refix",
|
|
404
|
+
"rework": "rework",
|
|
405
|
+
"re-work": "rework",
|
|
406
|
+
"mashup": "mashup",
|
|
407
|
+
"mash-up": "mashup",
|
|
408
|
+
"mash up": "mashup",
|
|
409
|
+
"blend": "blend",
|
|
410
|
+
"dub": "dub",
|
|
411
|
+
"version": "version",
|
|
412
|
+
"ver": "version",
|
|
413
|
+
"remaster": "remaster",
|
|
414
|
+
"remastered": "remaster",
|
|
415
|
+
"cover": "cover",
|
|
416
|
+
"reprise": "reprise"
|
|
417
|
+
},
|
|
418
|
+
"genericHeads": [
|
|
419
|
+
"mix",
|
|
420
|
+
"edit",
|
|
421
|
+
"version"
|
|
422
|
+
],
|
|
423
|
+
"typeCapableModifiers": {
|
|
424
|
+
"radio": "radio",
|
|
425
|
+
"extended": "extended",
|
|
426
|
+
"club": "club",
|
|
427
|
+
"original": "original",
|
|
428
|
+
"instrumental": "instrumental",
|
|
429
|
+
"dub": "dub",
|
|
430
|
+
"vip": "vip",
|
|
431
|
+
"acoustic": "acoustic",
|
|
432
|
+
"live": "live",
|
|
433
|
+
"acapella": "acapella",
|
|
434
|
+
"a cappella": "acapella",
|
|
435
|
+
"a capella": "acapella",
|
|
436
|
+
"acappella": "acapella",
|
|
437
|
+
"demo": "demo"
|
|
438
|
+
},
|
|
439
|
+
"prefixForms": {
|
|
440
|
+
"live": "live",
|
|
441
|
+
"remastered": "remaster",
|
|
442
|
+
"remaster": "remaster",
|
|
443
|
+
"acoustic": "acoustic",
|
|
444
|
+
"instrumental": "instrumental",
|
|
445
|
+
"acapella": "acapella",
|
|
446
|
+
"a cappella": "acapella",
|
|
447
|
+
"demo": "demo",
|
|
448
|
+
"sped up": "spedUp",
|
|
449
|
+
"speed up": "spedUp",
|
|
450
|
+
"slowed + reverb": "slowed",
|
|
451
|
+
"slowed & reverb": "slowed",
|
|
452
|
+
"slowed and reverb": "slowed",
|
|
453
|
+
"slowed": "slowed",
|
|
454
|
+
"nightcore": "nightcore"
|
|
455
|
+
}};
|
|
456
|
+
|
|
457
|
+
// src/words.ts
|
|
458
|
+
function isWhitespaceCode(cp) {
|
|
459
|
+
return cp >= 9 && cp <= 13 || cp === 32 || cp === 133 || cp === 160 || cp === 5760 || cp >= 8192 && cp <= 8202 || cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287 || cp === 12288;
|
|
460
|
+
}
|
|
461
|
+
function isWhitespace(ch) {
|
|
462
|
+
return ch !== void 0 && ch.length > 0 && isWhitespaceCode(ch.charCodeAt(0));
|
|
463
|
+
}
|
|
464
|
+
function isDigit(ch) {
|
|
465
|
+
return ch !== void 0 && ch.length === 1 && ch >= "0" && ch <= "9";
|
|
466
|
+
}
|
|
467
|
+
function isAllDigits(s) {
|
|
468
|
+
if (s.length === 0) return false;
|
|
469
|
+
for (let i = 0; i < s.length; i++) if (!isDigit(s[i])) return false;
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
var ALPHABETIC = /^\p{Alphabetic}$/u;
|
|
473
|
+
function isLetterAt(s, index) {
|
|
474
|
+
const cp = s.codePointAt(index);
|
|
475
|
+
if (cp === void 0) return false;
|
|
476
|
+
return ALPHABETIC.test(String.fromCodePoint(cp));
|
|
477
|
+
}
|
|
478
|
+
function asciiLower(s) {
|
|
479
|
+
let out = "";
|
|
480
|
+
for (let i = 0; i < s.length; i++) {
|
|
481
|
+
const c = s.charCodeAt(i);
|
|
482
|
+
out += c >= 65 && c <= 90 ? String.fromCharCode(c + 32) : s[i];
|
|
483
|
+
}
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
var KEY_STRIPPABLE = ".,!?:;";
|
|
487
|
+
function stripKeyPunct(lower) {
|
|
488
|
+
const last = lower[lower.length - 1];
|
|
489
|
+
return last !== void 0 && KEY_STRIPPABLE.includes(last) ? lower.slice(0, -1) : lower;
|
|
490
|
+
}
|
|
491
|
+
function wordKey(word) {
|
|
492
|
+
return stripKeyPunct(asciiLower(word));
|
|
493
|
+
}
|
|
494
|
+
function wordMatches(word, token) {
|
|
495
|
+
const lower = asciiLower(word);
|
|
496
|
+
return lower === token || stripKeyPunct(lower) === token;
|
|
497
|
+
}
|
|
498
|
+
function inVocab(word, vocab) {
|
|
499
|
+
const lower = asciiLower(word);
|
|
500
|
+
return vocab.has(lower) || vocab.has(stripKeyPunct(lower));
|
|
501
|
+
}
|
|
502
|
+
function collapseSpaces(s) {
|
|
503
|
+
let out = "";
|
|
504
|
+
let pendingSpace = false;
|
|
505
|
+
for (const ch of s) {
|
|
506
|
+
if (isWhitespaceCode(ch.codePointAt(0) ?? 0)) {
|
|
507
|
+
pendingSpace = out.length > 0;
|
|
508
|
+
} else {
|
|
509
|
+
if (pendingSpace) out += " ";
|
|
510
|
+
pendingSpace = false;
|
|
511
|
+
out += ch;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return out;
|
|
515
|
+
}
|
|
516
|
+
function splitWords(s) {
|
|
517
|
+
const words = [];
|
|
518
|
+
let current = "";
|
|
519
|
+
for (const ch of s) {
|
|
520
|
+
if (isWhitespaceCode(ch.codePointAt(0) ?? 0)) {
|
|
521
|
+
if (current) words.push(current);
|
|
522
|
+
current = "";
|
|
523
|
+
} else {
|
|
524
|
+
current += ch;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (current) words.push(current);
|
|
528
|
+
return words;
|
|
529
|
+
}
|
|
530
|
+
function wordSpans(s) {
|
|
531
|
+
const spans = [];
|
|
532
|
+
let start = -1;
|
|
533
|
+
for (let i = 0; i <= s.length; i++) {
|
|
534
|
+
const boundary = i === s.length || isWhitespace(s[i]);
|
|
535
|
+
if (boundary) {
|
|
536
|
+
if (start >= 0) spans.push({ text: s.slice(start, i), start, end: i });
|
|
537
|
+
start = -1;
|
|
538
|
+
} else if (start < 0) {
|
|
539
|
+
start = i;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return spans;
|
|
543
|
+
}
|
|
544
|
+
var COMBINING_START = 768;
|
|
545
|
+
var COMBINING_END = 879;
|
|
546
|
+
function dedupKey(s) {
|
|
547
|
+
let out = "";
|
|
548
|
+
for (const ch of s.toLowerCase().normalize("NFD")) {
|
|
549
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
550
|
+
if (cp >= COMBINING_START && cp <= COMBINING_END) continue;
|
|
551
|
+
out += ch;
|
|
552
|
+
}
|
|
553
|
+
return collapseSpaces(out);
|
|
554
|
+
}
|
|
555
|
+
function yearOf(word) {
|
|
556
|
+
const key = wordKey(word);
|
|
557
|
+
if (key.length !== 4 || !isAllDigits(key)) return null;
|
|
558
|
+
const n = Number(key);
|
|
559
|
+
return n >= 1900 && n <= 2099 ? n : null;
|
|
560
|
+
}
|
|
561
|
+
var PhraseTable = class {
|
|
562
|
+
constructor(entries) {
|
|
563
|
+
this.byFirst = /* @__PURE__ */ new Map();
|
|
564
|
+
this.byLast = /* @__PURE__ */ new Map();
|
|
565
|
+
this.keys = /* @__PURE__ */ new Map();
|
|
566
|
+
for (const [rawKey, value] of entries) this.add(rawKey, value);
|
|
567
|
+
}
|
|
568
|
+
add(rawKey, value) {
|
|
569
|
+
const key = asciiLower(rawKey).trim();
|
|
570
|
+
const tokens = splitWords(key);
|
|
571
|
+
const first = tokens[0];
|
|
572
|
+
const last = tokens[tokens.length - 1];
|
|
573
|
+
if (first === void 0 || last === void 0 || this.keys.has(key)) return;
|
|
574
|
+
this.keys.set(key, value);
|
|
575
|
+
const entry = { key, tokens, value };
|
|
576
|
+
insertSorted(this.byFirst, first, entry);
|
|
577
|
+
insertSorted(this.byLast, last, entry);
|
|
578
|
+
}
|
|
579
|
+
has(key) {
|
|
580
|
+
return this.keys.has(key);
|
|
581
|
+
}
|
|
582
|
+
/** Longest entry matching words[start …]. */
|
|
583
|
+
matchAt(words, start) {
|
|
584
|
+
const word = words[start];
|
|
585
|
+
if (word === void 0) return null;
|
|
586
|
+
let best = null;
|
|
587
|
+
for (const entry of candidates(this.byFirst, word)) {
|
|
588
|
+
const n = entry.tokens.length;
|
|
589
|
+
if (best && n <= best.length) continue;
|
|
590
|
+
if (start + n > words.length) continue;
|
|
591
|
+
if (tokensMatch(words, start, entry.tokens)) best = { entry, length: n };
|
|
592
|
+
}
|
|
593
|
+
return best;
|
|
594
|
+
}
|
|
595
|
+
/** Longest entry matching words[… end-1] (end exclusive). */
|
|
596
|
+
matchEnding(words, end) {
|
|
597
|
+
const word = words[end - 1];
|
|
598
|
+
if (word === void 0) return null;
|
|
599
|
+
let best = null;
|
|
600
|
+
for (const entry of candidates(this.byLast, word)) {
|
|
601
|
+
const n = entry.tokens.length;
|
|
602
|
+
if (best && n <= best.length) continue;
|
|
603
|
+
if (end - n < 0) continue;
|
|
604
|
+
if (tokensMatch(words, end - n, entry.tokens)) best = { entry, length: n };
|
|
605
|
+
}
|
|
606
|
+
return best;
|
|
607
|
+
}
|
|
608
|
+
/** The whole word list is exactly one entry. */
|
|
609
|
+
matchWhole(words) {
|
|
610
|
+
const m = this.matchAt(words, 0);
|
|
611
|
+
return m && m.length === words.length ? m : null;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
function insertSorted(map, k, e) {
|
|
615
|
+
const list = map.get(k) ?? [];
|
|
616
|
+
list.push(e);
|
|
617
|
+
list.sort((a, b) => b.tokens.length - a.tokens.length);
|
|
618
|
+
map.set(k, list);
|
|
619
|
+
}
|
|
620
|
+
function candidates(map, word) {
|
|
621
|
+
const lower = asciiLower(word);
|
|
622
|
+
const stripped = stripKeyPunct(lower);
|
|
623
|
+
const a = map.get(lower) ?? [];
|
|
624
|
+
if (stripped === lower) return a;
|
|
625
|
+
const b = map.get(stripped) ?? [];
|
|
626
|
+
return a.length === 0 ? b : b.length === 0 ? a : [...a, ...b];
|
|
627
|
+
}
|
|
628
|
+
function tokensMatch(words, start, tokens) {
|
|
629
|
+
for (let k = 0; k < tokens.length; k++) {
|
|
630
|
+
const w = words[start + k];
|
|
631
|
+
const t = tokens[k];
|
|
632
|
+
if (w === void 0 || t === void 0 || !wordMatches(w, t)) return false;
|
|
633
|
+
}
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/assemble.ts
|
|
638
|
+
var PRECEDENCE = { primary: 2, featured: 1, producer: 0, remixer: 0 };
|
|
639
|
+
function toArtist(c) {
|
|
640
|
+
return { name: c.name, role: c.role, joiner: c.joiner, source: c.source };
|
|
641
|
+
}
|
|
642
|
+
function passJoiner(entries, e, joiner) {
|
|
643
|
+
const members = entries.filter((x) => x.alive && x.list === e.list);
|
|
644
|
+
if (members[0] !== e) return;
|
|
645
|
+
const next = members[1];
|
|
646
|
+
if (next) next.a.joiner = joiner;
|
|
647
|
+
}
|
|
648
|
+
function dedupArtists(credits) {
|
|
649
|
+
const entries = credits.map((c, i) => ({
|
|
650
|
+
a: toArtist(c),
|
|
651
|
+
list: "list" in c ? c.list : -1 - i,
|
|
652
|
+
alive: true
|
|
653
|
+
}));
|
|
654
|
+
const index = /* @__PURE__ */ new Map();
|
|
655
|
+
for (const e of entries) {
|
|
656
|
+
const key = `${e.a.role === "producer" ? "p" : "c"}:${dedupKey(e.a.name)}`;
|
|
657
|
+
const kept = index.get(key);
|
|
658
|
+
if (!kept) {
|
|
659
|
+
index.set(key, e);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (PRECEDENCE[e.a.role] > PRECEDENCE[kept.a.role]) {
|
|
663
|
+
passJoiner(entries, kept, kept.a.joiner);
|
|
664
|
+
kept.a.role = e.a.role;
|
|
665
|
+
kept.a.joiner = e.a.joiner;
|
|
666
|
+
kept.a.source = e.a.source;
|
|
667
|
+
kept.list = e.list;
|
|
668
|
+
e.alive = false;
|
|
669
|
+
} else {
|
|
670
|
+
passJoiner(entries, e, e.a.joiner);
|
|
671
|
+
e.alive = false;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return entries.filter((e) => e.alive).map((e) => e.a);
|
|
675
|
+
}
|
|
676
|
+
function toVersion(v) {
|
|
677
|
+
return {
|
|
678
|
+
type: v.type,
|
|
679
|
+
raw: v.raw,
|
|
680
|
+
artists: v.artists.map(toArtist),
|
|
681
|
+
modifiers: [...v.modifiers],
|
|
682
|
+
descriptor: v.descriptor,
|
|
683
|
+
year: v.year,
|
|
684
|
+
unknownArtist: v.unknownArtist,
|
|
685
|
+
delimiter: v.delimiter
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
function renderVersion(v) {
|
|
689
|
+
switch (v.delimiter) {
|
|
690
|
+
case "(":
|
|
691
|
+
return `(${v.raw})`;
|
|
692
|
+
case "[":
|
|
693
|
+
return `[${v.raw}]`;
|
|
694
|
+
case "{":
|
|
695
|
+
return `{${v.raw}}`;
|
|
696
|
+
default:
|
|
697
|
+
return `- ${v.raw}`;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function fullTitleOf(title, versions) {
|
|
701
|
+
return [title, ...versions.map(renderVersion)].filter((s) => s.length > 0).join(" ");
|
|
702
|
+
}
|
|
703
|
+
function byPos(items) {
|
|
704
|
+
return [...items].sort((a, b) => a.pos - b.pos);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/tables.ts
|
|
708
|
+
function lowerKeys(list) {
|
|
709
|
+
return (list ?? []).map((s) => asciiLower(s).trim()).filter((s) => s.length > 0);
|
|
710
|
+
}
|
|
711
|
+
function buildTables(keywords) {
|
|
712
|
+
const featMarkers = new PhraseTable(
|
|
713
|
+
[...featMarkersData.markers, ...lowerKeys(keywords?.featMarkers)].map((m) => [m, true])
|
|
714
|
+
);
|
|
715
|
+
const bracketOnlyFeat = new PhraseTable(featMarkersData.bracketOnly.map((m) => [m, true]));
|
|
716
|
+
const producerMarkers = new PhraseTable(featMarkersData.producer.map((m) => [m, true]));
|
|
717
|
+
const titleFeatMarkers = new PhraseTable(
|
|
718
|
+
[...featMarkersData.titleMarkers, ...lowerKeys(keywords?.featMarkers)].map((m) => [m, true])
|
|
719
|
+
);
|
|
720
|
+
const titleProducerMarkers = new PhraseTable(
|
|
721
|
+
featMarkersData.titleProducer.map((m) => [m, true])
|
|
722
|
+
);
|
|
723
|
+
const junkEntries = [
|
|
724
|
+
...Object.entries(junkData.phrases),
|
|
725
|
+
...Object.entries(keywords?.junk ?? {})
|
|
726
|
+
];
|
|
727
|
+
const genres = [...genresData.genres, ...lowerKeys(keywords?.genres)];
|
|
728
|
+
const junkPhrases = new PhraseTable(junkEntries);
|
|
729
|
+
const junkOrGenre = new PhraseTable([
|
|
730
|
+
...junkEntries,
|
|
731
|
+
...genres.map((g) => [g, "genre"])
|
|
732
|
+
]);
|
|
733
|
+
const headEntries = [
|
|
734
|
+
...Object.entries(versionKeywordsData.heads),
|
|
735
|
+
...Object.entries(keywords?.versionHeads ?? {})
|
|
736
|
+
];
|
|
737
|
+
const heads = new PhraseTable(headEntries);
|
|
738
|
+
const runInfo = /* @__PURE__ */ new Map();
|
|
739
|
+
const note = (key, patch) => {
|
|
740
|
+
const k = asciiLower(key).trim();
|
|
741
|
+
runInfo.set(k, { ...runInfo.get(k), ...patch });
|
|
742
|
+
};
|
|
743
|
+
for (const [k, t] of Object.entries(versionKeywordsData.typeCapableModifiers)) {
|
|
744
|
+
note(k, { typeCapable: t });
|
|
745
|
+
}
|
|
746
|
+
for (const d of [...descriptorsData.descriptors, ...lowerKeys(keywords?.descriptors)]) {
|
|
747
|
+
note(d, { descriptor: asciiLower(d).trim() });
|
|
748
|
+
}
|
|
749
|
+
for (const g of genres) note(g, { genre: true });
|
|
750
|
+
for (const [k, t] of headEntries) note(k, { head: t });
|
|
751
|
+
return {
|
|
752
|
+
featMarkers,
|
|
753
|
+
bracketOnlyFeat,
|
|
754
|
+
producerMarkers,
|
|
755
|
+
titleFeatMarkers,
|
|
756
|
+
titleProducerMarkers,
|
|
757
|
+
junkOrGenre,
|
|
758
|
+
junkPhrases,
|
|
759
|
+
junkConnectors: new Set(junkData.connectors),
|
|
760
|
+
labelSuffixes: new Set(junkData.labelSuffixes),
|
|
761
|
+
heads,
|
|
762
|
+
genericHeadTypes: new Set(versionKeywordsData.genericHeads),
|
|
763
|
+
runWords: new PhraseTable(runInfo),
|
|
764
|
+
prefixForms: new PhraseTable(
|
|
765
|
+
Object.entries(versionKeywordsData.prefixForms)
|
|
766
|
+
),
|
|
767
|
+
spacedJoiners: joinersData.spaced.map((j) => ({
|
|
768
|
+
raw: j.raw,
|
|
769
|
+
canonical: j.canonical,
|
|
770
|
+
isAnd: "kind" in j && j.kind === "and",
|
|
771
|
+
caseSensitive: "caseSensitive" in j && j.caseSensitive === true
|
|
772
|
+
})),
|
|
773
|
+
tightJoiners: new Map(joinersData.tight.map((j) => [j.raw, j.canonical])),
|
|
774
|
+
featCanonical: joinersData.featCanonical,
|
|
775
|
+
noSplitBefore: new Set(noSplitBeforeData.words),
|
|
776
|
+
stopwords: new Set(stopwordsData.words),
|
|
777
|
+
unknownCaseSensitive: new Set(unknownTokensData.caseSensitive),
|
|
778
|
+
unknownCaseInsensitive: new Set(unknownTokensData.caseInsensitive),
|
|
779
|
+
extensions: new Set(extensionsData.extensions),
|
|
780
|
+
platformSuffixes: [...platformSuffixesData.suffixes].sort((a, b) => b.length - a.length)
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
var defaultTables = null;
|
|
784
|
+
function getDefaultTables() {
|
|
785
|
+
defaultTables ?? (defaultTables = buildTables());
|
|
786
|
+
return defaultTables;
|
|
787
|
+
}
|
|
788
|
+
function tablesFor(keywords) {
|
|
789
|
+
return keywords ? buildTables(keywords) : getDefaultTables();
|
|
790
|
+
}
|
|
791
|
+
function isUnknownToken(text, t) {
|
|
792
|
+
return t.unknownCaseSensitive.has(text) || t.unknownCaseInsensitive.has(asciiLower(text));
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// src/format.ts
|
|
796
|
+
function resolve(o) {
|
|
797
|
+
return {
|
|
798
|
+
feat: o?.feat ?? "source",
|
|
799
|
+
featMarker: o?.featMarker,
|
|
800
|
+
canonical: o?.joiners === "canonical",
|
|
801
|
+
versions: (o?.versions ?? "all") === "all",
|
|
802
|
+
producers: o?.producers ?? true,
|
|
803
|
+
position: o?.position ?? false
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function isFeatMarker(raw, t) {
|
|
807
|
+
return t.featMarkers.matchAt([raw], 0) !== null || t.bracketOnlyFeat.matchAt([raw], 0) !== null;
|
|
808
|
+
}
|
|
809
|
+
function canonicalJoiner(raw, t, featLead = false) {
|
|
810
|
+
if (featLead && isFeatMarker(raw, t)) return t.featCanonical;
|
|
811
|
+
const tight = t.tightJoiners.get(raw);
|
|
812
|
+
if (tight !== void 0) return tight;
|
|
813
|
+
for (const j of t.spacedJoiners) {
|
|
814
|
+
if (j.caseSensitive ? raw === j.raw : wordMatches(raw, j.raw)) return j.canonical;
|
|
815
|
+
}
|
|
816
|
+
return isFeatMarker(raw, t) ? t.featCanonical : raw;
|
|
817
|
+
}
|
|
818
|
+
function joinSep(joiner, o, t) {
|
|
819
|
+
const j = joiner === null ? "&" : o.canonical ? canonicalJoiner(joiner, t) : joiner;
|
|
820
|
+
return t.tightJoiners.has(j) ? `${j} ` : ` ${j} `;
|
|
821
|
+
}
|
|
822
|
+
function joinNames(list, o, t) {
|
|
823
|
+
return list.map((a, i) => i === 0 ? a.name : joinSep(a.joiner, o, t) + a.name).join("");
|
|
824
|
+
}
|
|
825
|
+
function leadMarker(first, o, t, fallback) {
|
|
826
|
+
if (o.featMarker !== void 0) return o.featMarker;
|
|
827
|
+
const raw = first.joiner ?? fallback;
|
|
828
|
+
return o.canonical ? canonicalJoiner(raw, t, true) : raw;
|
|
829
|
+
}
|
|
830
|
+
function format(track, options) {
|
|
831
|
+
const t = getDefaultTables();
|
|
832
|
+
const o = resolve(options);
|
|
833
|
+
const artists = Array.isArray(track?.artists) ? track.artists : [];
|
|
834
|
+
const primaries = artists.filter((a) => a.role === "primary");
|
|
835
|
+
const featured = artists.filter((a) => a.role === "featured");
|
|
836
|
+
const producers = artists.filter((a) => a.role === "producer");
|
|
837
|
+
let artistFeat = [];
|
|
838
|
+
let titleFeat = [];
|
|
839
|
+
if (o.feat === "source") {
|
|
840
|
+
artistFeat = featured.filter((a) => a.source === "artist");
|
|
841
|
+
titleFeat = featured.filter((a) => a.source !== "artist");
|
|
842
|
+
} else if (o.feat === "artist") {
|
|
843
|
+
artistFeat = featured;
|
|
844
|
+
} else if (o.feat === "title") {
|
|
845
|
+
titleFeat = featured;
|
|
846
|
+
}
|
|
847
|
+
let artistPart = joinNames(primaries, o, t);
|
|
848
|
+
const artistFeatFirst = artistFeat[0];
|
|
849
|
+
if (artistFeatFirst) {
|
|
850
|
+
const lead = leadMarker(artistFeatFirst, o, t, t.featCanonical);
|
|
851
|
+
const names = joinNames(artistFeat, o, t);
|
|
852
|
+
artistPart = artistPart ? `${artistPart} ${lead} ${names}` : `${lead} ${names}`;
|
|
853
|
+
}
|
|
854
|
+
const pieces = [];
|
|
855
|
+
if (track?.title) pieces.push(track.title);
|
|
856
|
+
const titleFeatFirst = titleFeat[0];
|
|
857
|
+
if (titleFeatFirst) {
|
|
858
|
+
const lead = leadMarker(titleFeatFirst, o, t, t.featCanonical);
|
|
859
|
+
pieces.push(`(${lead} ${joinNames(titleFeat, o, t)})`);
|
|
860
|
+
}
|
|
861
|
+
if (o.versions) for (const v of track?.versions ?? []) pieces.push(renderVersion(v));
|
|
862
|
+
const producerFirst = producers[0];
|
|
863
|
+
if (o.producers && producerFirst) {
|
|
864
|
+
const marker = producerFirst.joiner ?? "prod.";
|
|
865
|
+
pieces.push(`(${marker} ${joinNames(producers, o, t)})`);
|
|
866
|
+
}
|
|
867
|
+
const titlePart = pieces.join(" ");
|
|
868
|
+
let result = artistPart ? `${artistPart} - ${titlePart}` : titlePart;
|
|
869
|
+
if (o.position && track?.position) result = `${track.position.raw}. ${result}`;
|
|
870
|
+
return result;
|
|
871
|
+
}
|
|
872
|
+
function allArtists(track) {
|
|
873
|
+
const seen = /* @__PURE__ */ new Set();
|
|
874
|
+
const out = [];
|
|
875
|
+
const all = [...track?.artists ?? [], ...(track?.versions ?? []).flatMap((v) => v.artists)];
|
|
876
|
+
for (const a of all) {
|
|
877
|
+
const key = dedupKey(a.name);
|
|
878
|
+
if (seen.has(key)) continue;
|
|
879
|
+
seen.add(key);
|
|
880
|
+
out.push(a);
|
|
881
|
+
}
|
|
882
|
+
return out;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/normalize.ts
|
|
886
|
+
var REMOVED = /* @__PURE__ */ new Set([8203, 8288, 65279, 173, 57344]);
|
|
887
|
+
var CHAR_MAP = /* @__PURE__ */ new Map([
|
|
888
|
+
[65288, "("],
|
|
889
|
+
[65289, ")"],
|
|
890
|
+
[65339, "["],
|
|
891
|
+
[12304, "["],
|
|
892
|
+
[12308, "["],
|
|
893
|
+
[65341, "]"],
|
|
894
|
+
[12305, "]"],
|
|
895
|
+
[12309, "]"],
|
|
896
|
+
[65371, "{"],
|
|
897
|
+
[65373, "}"],
|
|
898
|
+
[8220, '"'],
|
|
899
|
+
[8221, '"'],
|
|
900
|
+
[8222, '"'],
|
|
901
|
+
[171, '"'],
|
|
902
|
+
[187, '"'],
|
|
903
|
+
[12300, '"'],
|
|
904
|
+
[12301, '"'],
|
|
905
|
+
[12302, '"'],
|
|
906
|
+
[12303, '"'],
|
|
907
|
+
[65282, '"'],
|
|
908
|
+
[8216, "'"],
|
|
909
|
+
[8217, "'"],
|
|
910
|
+
[8218, "'"],
|
|
911
|
+
[96, "'"],
|
|
912
|
+
[180, "'"],
|
|
913
|
+
[8722, "-"],
|
|
914
|
+
[65112, "-"],
|
|
915
|
+
[65123, "-"],
|
|
916
|
+
[65293, "-"]
|
|
917
|
+
]);
|
|
918
|
+
function mapChar(cp) {
|
|
919
|
+
if (REMOVED.has(cp)) return null;
|
|
920
|
+
if (cp >= 8208 && cp <= 8213) return "-";
|
|
921
|
+
return CHAR_MAP.get(cp) ?? String.fromCodePoint(cp);
|
|
922
|
+
}
|
|
923
|
+
function collapseSpacedDashRuns(chars) {
|
|
924
|
+
const out = [];
|
|
925
|
+
let i = 0;
|
|
926
|
+
while (i < chars.length) {
|
|
927
|
+
if (chars[i] !== "-") {
|
|
928
|
+
out.push(chars[i]);
|
|
929
|
+
i++;
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
let j = i;
|
|
933
|
+
while (chars[j] === "-") j++;
|
|
934
|
+
const before = chars[i - 1];
|
|
935
|
+
const after = chars[j];
|
|
936
|
+
const spaced = j - i >= 2 && before !== void 0 && after !== void 0 && isWhitespaceCode(before.codePointAt(0) ?? 0) && isWhitespaceCode(after.codePointAt(0) ?? 0);
|
|
937
|
+
if (spaced) out.push("-");
|
|
938
|
+
else for (let k = i; k < j; k++) out.push("-");
|
|
939
|
+
i = j;
|
|
940
|
+
}
|
|
941
|
+
return out;
|
|
942
|
+
}
|
|
943
|
+
function collapseWhitespace(chars) {
|
|
944
|
+
let out = "";
|
|
945
|
+
let pending = false;
|
|
946
|
+
for (const ch of chars) {
|
|
947
|
+
if (isWhitespaceCode(ch.codePointAt(0) ?? 0)) {
|
|
948
|
+
pending = out.length > 0;
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
if (pending) out += " ";
|
|
952
|
+
pending = false;
|
|
953
|
+
out += ch;
|
|
954
|
+
}
|
|
955
|
+
return out;
|
|
956
|
+
}
|
|
957
|
+
function normalize(input) {
|
|
958
|
+
if (typeof input !== "string") return "";
|
|
959
|
+
const chars = [];
|
|
960
|
+
for (const ch of input.normalize("NFC")) {
|
|
961
|
+
const mapped = mapChar(ch.codePointAt(0) ?? 0);
|
|
962
|
+
if (mapped !== null) chars.push(mapped);
|
|
963
|
+
}
|
|
964
|
+
return collapseWhitespace(collapseSpacedDashRuns(chars)).normalize("NFC");
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// src/context.ts
|
|
968
|
+
function makeCtx(t, options) {
|
|
969
|
+
const known = (options?.knownArtists ?? []).filter((k) => typeof k === "string");
|
|
970
|
+
const knownKeys = new Set(known.map(dedupKey).filter((k) => k.length > 0));
|
|
971
|
+
const knownMaxLen = known.reduce((m, k) => Math.max(m, k.length), 0);
|
|
972
|
+
const splitAnd = options?.splitAnd ?? "auto";
|
|
973
|
+
return { t, knownKeys, knownMaxLen, splitAnd, warnings: [], listSeq: 0 };
|
|
974
|
+
}
|
|
975
|
+
function warn(ctx, w) {
|
|
976
|
+
if (!ctx.warnings.includes(w)) ctx.warnings.push(w);
|
|
977
|
+
}
|
|
978
|
+
function matchKnownAt(text, start, ctx, extraBoundary = "") {
|
|
979
|
+
if (ctx.knownKeys.size === 0) return 0;
|
|
980
|
+
const limit = Math.min(text.length, start + ctx.knownMaxLen * 2 + 8);
|
|
981
|
+
for (let end = limit; end > start; end--) {
|
|
982
|
+
const next = text[end];
|
|
983
|
+
const boundary = end === text.length || next === "," || next === ";" || isWhitespace(next) || extraBoundary !== "" && next === extraBoundary;
|
|
984
|
+
if (!boundary) continue;
|
|
985
|
+
if (ctx.knownKeys.has(dedupKey(text.slice(start, end)))) return end - start;
|
|
986
|
+
}
|
|
987
|
+
return 0;
|
|
988
|
+
}
|
|
989
|
+
function isKnownArtist(text, ctx) {
|
|
990
|
+
return ctx.knownKeys.size > 0 && ctx.knownKeys.has(dedupKey(text));
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// src/scanner.ts
|
|
994
|
+
var PH = "\uE000";
|
|
995
|
+
var CLOSER = { "(": ")", "[": "]", "{": "}" };
|
|
996
|
+
function isOpener(ch) {
|
|
997
|
+
return ch === "(" || ch === "[" || ch === "{";
|
|
998
|
+
}
|
|
999
|
+
function scan(str, base = 0) {
|
|
1000
|
+
let text = "";
|
|
1001
|
+
const pos = [];
|
|
1002
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1003
|
+
const stack = [];
|
|
1004
|
+
let groupStart = -1;
|
|
1005
|
+
for (let i = 0; i < str.length; i++) {
|
|
1006
|
+
const ch = str[i];
|
|
1007
|
+
if (stack.length === 0) {
|
|
1008
|
+
if (isOpener(ch)) {
|
|
1009
|
+
stack.push(CLOSER[ch]);
|
|
1010
|
+
groupStart = i;
|
|
1011
|
+
} else {
|
|
1012
|
+
text += ch;
|
|
1013
|
+
pos.push(base + i);
|
|
1014
|
+
}
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
if (isOpener(ch)) {
|
|
1018
|
+
stack.push(CLOSER[ch]);
|
|
1019
|
+
} else if (ch === stack[stack.length - 1]) {
|
|
1020
|
+
stack.pop();
|
|
1021
|
+
if (stack.length === 0) {
|
|
1022
|
+
addGroup(str, groupStart, i + 1, true, base, groups);
|
|
1023
|
+
text += PH;
|
|
1024
|
+
pos.push(base + groupStart);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
const unbalanced = stack.length > 0;
|
|
1029
|
+
if (unbalanced) {
|
|
1030
|
+
addGroup(str, groupStart, str.length, false, base, groups);
|
|
1031
|
+
text += PH;
|
|
1032
|
+
pos.push(base + groupStart);
|
|
1033
|
+
}
|
|
1034
|
+
return { skel: { text, pos, groups }, unbalanced };
|
|
1035
|
+
}
|
|
1036
|
+
function addGroup(str, start, end, closed, base, groups) {
|
|
1037
|
+
const open = str[start];
|
|
1038
|
+
const inner = str.slice(start + 1, closed ? end - 1 : end);
|
|
1039
|
+
groups.set(base + start, {
|
|
1040
|
+
open,
|
|
1041
|
+
inner: collapseSpaces(inner),
|
|
1042
|
+
raw: str.slice(start, end),
|
|
1043
|
+
closed,
|
|
1044
|
+
pos: base + start,
|
|
1045
|
+
end: base + end
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
function sliceSkel(s, start, end = s.text.length) {
|
|
1049
|
+
const a = Math.max(0, Math.min(start, s.text.length));
|
|
1050
|
+
const b = Math.max(a, Math.min(end, s.text.length));
|
|
1051
|
+
return { text: s.text.slice(a, b), pos: s.pos.slice(a, b), groups: s.groups };
|
|
1052
|
+
}
|
|
1053
|
+
function trimSkel(s) {
|
|
1054
|
+
let a = 0;
|
|
1055
|
+
let b = s.text.length;
|
|
1056
|
+
while (a < b && isWhitespace(s.text[a])) a++;
|
|
1057
|
+
while (b > a && isWhitespace(s.text[b - 1])) b--;
|
|
1058
|
+
return sliceSkel(s, a, b);
|
|
1059
|
+
}
|
|
1060
|
+
function concatSkel(a, b) {
|
|
1061
|
+
return { text: a.text + b.text, pos: [...a.pos, ...b.pos], groups: a.groups };
|
|
1062
|
+
}
|
|
1063
|
+
function cutSkel(s, start, end) {
|
|
1064
|
+
return concatSkel(sliceSkel(s, 0, start), sliceSkel(s, end));
|
|
1065
|
+
}
|
|
1066
|
+
function groupAt(s, index) {
|
|
1067
|
+
if (s.text[index] !== PH) return void 0;
|
|
1068
|
+
const p = s.pos[index];
|
|
1069
|
+
return p === void 0 ? void 0 : s.groups.get(p);
|
|
1070
|
+
}
|
|
1071
|
+
function groupsOf(s) {
|
|
1072
|
+
const out = [];
|
|
1073
|
+
for (let i = 0; i < s.text.length; i++) {
|
|
1074
|
+
const g = groupAt(s, i);
|
|
1075
|
+
if (g) out.push({ index: i, group: g });
|
|
1076
|
+
}
|
|
1077
|
+
return out;
|
|
1078
|
+
}
|
|
1079
|
+
function hasGroup(s) {
|
|
1080
|
+
return s.text.includes(PH);
|
|
1081
|
+
}
|
|
1082
|
+
function renderSkel(s) {
|
|
1083
|
+
if (!s.text.includes(PH)) return s.text;
|
|
1084
|
+
let out = "";
|
|
1085
|
+
for (let i = 0; i < s.text.length; i++) {
|
|
1086
|
+
const g = groupAt(s, i);
|
|
1087
|
+
out += g ? g.raw : s.text[i];
|
|
1088
|
+
}
|
|
1089
|
+
return out;
|
|
1090
|
+
}
|
|
1091
|
+
function removeGroups(s, remove) {
|
|
1092
|
+
if (remove.size === 0) return s;
|
|
1093
|
+
let text = "";
|
|
1094
|
+
const pos = [];
|
|
1095
|
+
for (let i = 0; i < s.text.length; i++) {
|
|
1096
|
+
const p = s.pos[i];
|
|
1097
|
+
if (s.text[i] === PH && remove.has(p)) continue;
|
|
1098
|
+
text += s.text[i];
|
|
1099
|
+
pos.push(p);
|
|
1100
|
+
}
|
|
1101
|
+
return { text, pos, groups: s.groups };
|
|
1102
|
+
}
|
|
1103
|
+
function isSpacedDashAt(text, i) {
|
|
1104
|
+
return text[i] === "-" && text[i - 1] === " " && text[i + 1] === " ";
|
|
1105
|
+
}
|
|
1106
|
+
function hasSpacedDash(text) {
|
|
1107
|
+
for (let i = 1; i < text.length - 1; i++) if (isSpacedDashAt(text, i)) return true;
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
function trimmedSegment(s, start, end) {
|
|
1111
|
+
let a = start;
|
|
1112
|
+
let b = Math.max(start, end);
|
|
1113
|
+
while (a < b && isWhitespace(s.text[a])) a++;
|
|
1114
|
+
while (b > a && isWhitespace(s.text[b - 1])) b--;
|
|
1115
|
+
return { skel: sliceSkel(s, a, b), start: a, end: b };
|
|
1116
|
+
}
|
|
1117
|
+
function splitAtSpacedDashes(s, allowLeading = false) {
|
|
1118
|
+
const segments = [];
|
|
1119
|
+
const t = s.text;
|
|
1120
|
+
let start = 0;
|
|
1121
|
+
if (allowLeading && t[0] === "-" && t[1] === " ") {
|
|
1122
|
+
segments.push({ skel: sliceSkel(s, 0, 0), start: 0, end: 0 });
|
|
1123
|
+
start = 2;
|
|
1124
|
+
}
|
|
1125
|
+
for (let i = Math.max(start, 1); i < t.length - 1; i++) {
|
|
1126
|
+
if (isSpacedDashAt(t, i)) {
|
|
1127
|
+
segments.push(trimmedSegment(s, start, i - 1));
|
|
1128
|
+
start = i + 2;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
segments.push(trimmedSegment(s, start, t.length));
|
|
1132
|
+
return segments;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// src/credits.ts
|
|
1136
|
+
var AND_LIKE = /* @__PURE__ */ new Set(["&", "and", "+"]);
|
|
1137
|
+
function wordAt(text, start) {
|
|
1138
|
+
let end = start;
|
|
1139
|
+
while (end < text.length && !isWhitespace(text[end])) end++;
|
|
1140
|
+
return end;
|
|
1141
|
+
}
|
|
1142
|
+
function nextWord(text, from) {
|
|
1143
|
+
let a = from;
|
|
1144
|
+
while (a < text.length && isWhitespace(text[a])) a++;
|
|
1145
|
+
let b = a;
|
|
1146
|
+
while (b < text.length && !isWhitespace(text[b]) && text[b] !== "," && text[b] !== ";") b++;
|
|
1147
|
+
return text.slice(a, b);
|
|
1148
|
+
}
|
|
1149
|
+
function findJoiner(text, from, splitAtComma, opts, ctx) {
|
|
1150
|
+
for (let p = from; p < text.length; p++) {
|
|
1151
|
+
const ch = text[p];
|
|
1152
|
+
if (ch === "," || ch === ";") {
|
|
1153
|
+
if (ch === "," && isDigit(text[p - 1]) && isDigit(text[p + 1])) continue;
|
|
1154
|
+
return { start: p, end: p + 1, raw: ch };
|
|
1155
|
+
}
|
|
1156
|
+
if (p === 0 || text[p - 1] !== " " || isWhitespace(ch)) continue;
|
|
1157
|
+
const end = wordAt(text, p);
|
|
1158
|
+
if (end >= text.length || text[end] !== " ") continue;
|
|
1159
|
+
const word = text.slice(p, end);
|
|
1160
|
+
const hit = spacedJoiner(word, text, end, splitAtComma, opts, ctx);
|
|
1161
|
+
if (hit) return { start: p, end, raw: word };
|
|
1162
|
+
}
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
function spacedJoiner(word, text, end, splitAtComma, opts, ctx) {
|
|
1166
|
+
if (opts.featMarkersJoin && ctx.t.featMarkers.matchAt([word], 0)) return true;
|
|
1167
|
+
for (const j of ctx.t.spacedJoiners) {
|
|
1168
|
+
const matches = j.caseSensitive ? word === j.raw : wordMatches(word, j.raw);
|
|
1169
|
+
if (!matches) continue;
|
|
1170
|
+
if (j.raw === "with" && !opts.splitWith) return false;
|
|
1171
|
+
if (j.isAnd) {
|
|
1172
|
+
if (ctx.splitAnd === "never") return false;
|
|
1173
|
+
if (ctx.splitAnd === "auto" && !splitAtComma) return false;
|
|
1174
|
+
}
|
|
1175
|
+
if (AND_LIKE.has(j.raw) && inVocab(nextWord(text, end), ctx.t.noSplitBefore)) return false;
|
|
1176
|
+
return true;
|
|
1177
|
+
}
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
function cleanName(raw) {
|
|
1181
|
+
let name = collapseSpaces(raw);
|
|
1182
|
+
const first = name[0];
|
|
1183
|
+
if (name.length >= 2 && (first === '"' || first === "'") && name[name.length - 1] === first) {
|
|
1184
|
+
name = collapseSpaces(name.slice(1, -1));
|
|
1185
|
+
}
|
|
1186
|
+
while (name.endsWith(",") || name.endsWith(";")) name = name.slice(0, -1).trimEnd();
|
|
1187
|
+
const space = name.indexOf(" ");
|
|
1188
|
+
if (space > 0 && wordKey(name.slice(0, space)) === "by") name = name.slice(space + 1);
|
|
1189
|
+
return collapseSpaces(name);
|
|
1190
|
+
}
|
|
1191
|
+
function unwrapList(list) {
|
|
1192
|
+
const t = trimSkel(list);
|
|
1193
|
+
const q = t.text[0];
|
|
1194
|
+
if (t.text.length < 2 || q !== '"' && q !== "'" || t.text[t.text.length - 1] !== q) return t;
|
|
1195
|
+
if (t.text.indexOf(q, 1) !== t.text.length - 1) return t;
|
|
1196
|
+
return trimSkel(sliceSkel(t, 1, t.text.length - 1));
|
|
1197
|
+
}
|
|
1198
|
+
function splitCredits(input, opts, ctx) {
|
|
1199
|
+
const list = unwrapList(input);
|
|
1200
|
+
const text = list.text;
|
|
1201
|
+
const raw = [];
|
|
1202
|
+
let joiner = opts.lead;
|
|
1203
|
+
let splitAtComma = false;
|
|
1204
|
+
let start = 0;
|
|
1205
|
+
while (start <= text.length) {
|
|
1206
|
+
while (start < text.length && isWhitespace(text[start])) start++;
|
|
1207
|
+
const known = matchKnownAt(text, start, ctx, "-");
|
|
1208
|
+
const hit = findJoiner(text, start + known, splitAtComma, opts, ctx);
|
|
1209
|
+
const end = hit ? hit.start : text.length;
|
|
1210
|
+
raw.push({
|
|
1211
|
+
name: cleanName(renderSkel(sliceSkel(list, start, end))),
|
|
1212
|
+
joiner,
|
|
1213
|
+
pos: list.pos[start] ?? list.pos[list.pos.length - 1] ?? 0
|
|
1214
|
+
});
|
|
1215
|
+
if (!hit) break;
|
|
1216
|
+
if (hit.raw === "," || hit.raw === ";") splitAtComma = true;
|
|
1217
|
+
joiner = hit.raw;
|
|
1218
|
+
start = hit.end;
|
|
1219
|
+
}
|
|
1220
|
+
return finishNames(raw, opts, ctx);
|
|
1221
|
+
}
|
|
1222
|
+
function finishNames(raw, opts, ctx) {
|
|
1223
|
+
const named = raw.filter((r) => r.name.length > 0);
|
|
1224
|
+
let unknown = false;
|
|
1225
|
+
const years = [];
|
|
1226
|
+
const kept = named.filter((r) => {
|
|
1227
|
+
if (isUnknownToken(r.name, ctx.t)) {
|
|
1228
|
+
unknown = true;
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
const year = isAllDigits(r.name) ? yearOf(r.name) : null;
|
|
1232
|
+
if (year !== null && named.length > 1) {
|
|
1233
|
+
years.push({ year, pos: r.pos });
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
return true;
|
|
1237
|
+
});
|
|
1238
|
+
const list = ctx.listSeq++;
|
|
1239
|
+
const credits = kept.map(
|
|
1240
|
+
(r, i) => ({
|
|
1241
|
+
name: r.name,
|
|
1242
|
+
role: opts.role,
|
|
1243
|
+
joiner: i === 0 ? opts.lead : r.joiner,
|
|
1244
|
+
source: opts.source,
|
|
1245
|
+
pos: r.pos,
|
|
1246
|
+
list
|
|
1247
|
+
})
|
|
1248
|
+
);
|
|
1249
|
+
return { credits, unknown, years };
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// src/classify.ts
|
|
1253
|
+
var EXPLICIT_FLAGS = /* @__PURE__ */ new Set(["explicit", "explicit version", "dirty", "dirty version"]);
|
|
1254
|
+
var CLEAN_FLAGS = /* @__PURE__ */ new Set(["clean", "clean version", "radio clean"]);
|
|
1255
|
+
function classify(inner, site, ctx) {
|
|
1256
|
+
const words = splitWords(inner);
|
|
1257
|
+
if (words.length === 0) return { kind: "unknown" };
|
|
1258
|
+
const junkKind = junkKindOf(words, ctx);
|
|
1259
|
+
if (junkKind) return { kind: "junk", junkKind };
|
|
1260
|
+
const credit = featOrProducer(inner, site, ctx);
|
|
1261
|
+
if (credit) return credit;
|
|
1262
|
+
const flag = flagOf(words);
|
|
1263
|
+
if (flag) return { kind: "flag", flag };
|
|
1264
|
+
if (words.length === 1 && isAllDigits(words[0])) {
|
|
1265
|
+
const year = yearOf(words[0]);
|
|
1266
|
+
if (year !== null) return { kind: "year", year };
|
|
1267
|
+
}
|
|
1268
|
+
const versions = multiVersion(inner, site, ctx) ?? singleVersion(inner, site, ctx);
|
|
1269
|
+
if (versions) return { kind: "versions", versions };
|
|
1270
|
+
return { kind: "unknown" };
|
|
1271
|
+
}
|
|
1272
|
+
function junkKindOf(words, ctx) {
|
|
1273
|
+
let firstKind = null;
|
|
1274
|
+
let i = 0;
|
|
1275
|
+
while (i < words.length) {
|
|
1276
|
+
const m = ctx.t.junkOrGenre.matchAt(words, i);
|
|
1277
|
+
if (m) {
|
|
1278
|
+
firstKind ?? (firstKind = m.entry.value);
|
|
1279
|
+
i += m.length;
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
const w = words[i];
|
|
1283
|
+
if (ctx.t.junkConnectors.has(w) || ctx.t.junkConnectors.has(wordKey(w)) || yearOf(w) !== null) {
|
|
1284
|
+
i++;
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
firstKind = null;
|
|
1288
|
+
break;
|
|
1289
|
+
}
|
|
1290
|
+
if (firstKind) return firstKind;
|
|
1291
|
+
const last = words[words.length - 1];
|
|
1292
|
+
if (words.length >= 2 && last !== void 0 && inVocab(last, ctx.t.labelSuffixes)) return "label";
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
function featOrProducer(inner, site, ctx) {
|
|
1296
|
+
const spans = wordSpans(inner);
|
|
1297
|
+
const words = spans.map((s) => s.text);
|
|
1298
|
+
const feat = ctx.t.featMarkers.matchAt(words, 0);
|
|
1299
|
+
const bracketOnly = feat ? null : ctx.t.bracketOnlyFeat.matchAt(words, 0);
|
|
1300
|
+
const featLen = feat?.length ?? bracketOnly?.length ?? 0;
|
|
1301
|
+
if (featLen > 0 && words.length > featLen) {
|
|
1302
|
+
const next = words[featLen];
|
|
1303
|
+
if (!bracketOnly || !inVocab(next, ctx.t.stopwords)) {
|
|
1304
|
+
const r = creditsAfter(inner, spans, featLen, "featured", site, ctx);
|
|
1305
|
+
return { kind: "feat", ...r };
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
const prod = ctx.t.producerMarkers.matchAt(words, 0);
|
|
1309
|
+
if (prod) {
|
|
1310
|
+
let len = prod.length;
|
|
1311
|
+
if (words[len] !== void 0 && wordKey(words[len]) === "by") len++;
|
|
1312
|
+
if (words.length > len) {
|
|
1313
|
+
const r = creditsAfter(inner, spans, len, "producer", site, ctx);
|
|
1314
|
+
return { kind: "producer", ...r };
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
function creditsAfter(inner, spans, markerWords, role, site, ctx) {
|
|
1320
|
+
const lastMarker = spans[markerWords - 1];
|
|
1321
|
+
const firstName = spans[markerWords];
|
|
1322
|
+
const lead = inner.slice(spans[0].start, lastMarker.end);
|
|
1323
|
+
return listFromText(inner.slice(firstName.start), site.pos + 1 + firstName.start, ctx, {
|
|
1324
|
+
role,
|
|
1325
|
+
source: site.source,
|
|
1326
|
+
lead,
|
|
1327
|
+
splitWith: false,
|
|
1328
|
+
featMarkersJoin: role === "featured"
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
function listFromText(text, base, ctx, opts) {
|
|
1332
|
+
return splitCredits(scan(text, base).skel, opts, ctx);
|
|
1333
|
+
}
|
|
1334
|
+
function flagOf(words) {
|
|
1335
|
+
const key = words.map(wordKey).join(" ");
|
|
1336
|
+
if (EXPLICIT_FLAGS.has(key)) return "explicit";
|
|
1337
|
+
if (CLEAN_FLAGS.has(key)) return "clean";
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
function splitMultiParts(inner) {
|
|
1341
|
+
const parts = [];
|
|
1342
|
+
let depth = 0;
|
|
1343
|
+
let start = 0;
|
|
1344
|
+
for (let i = 0; i < inner.length; i++) {
|
|
1345
|
+
const ch = inner[i];
|
|
1346
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
1347
|
+
else if ((ch === ")" || ch === "]" || ch === "}") && depth > 0) depth--;
|
|
1348
|
+
else if (depth === 0 && ch === "/" && inner[i - 1] === " " && inner[i + 1] === " ") {
|
|
1349
|
+
parts.push(inner.slice(start, i - 1));
|
|
1350
|
+
start = i + 2;
|
|
1351
|
+
} else if (depth === 0 && ch === ";" && inner[i + 1] === " ") {
|
|
1352
|
+
parts.push(inner.slice(start, i));
|
|
1353
|
+
start = i + 2;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
parts.push(inner.slice(start));
|
|
1357
|
+
return parts.map((p) => p.trim());
|
|
1358
|
+
}
|
|
1359
|
+
function multiVersion(inner, site, ctx) {
|
|
1360
|
+
const parts = splitMultiParts(inner);
|
|
1361
|
+
if (parts.length < 2) return null;
|
|
1362
|
+
const out = [];
|
|
1363
|
+
for (const part of parts) {
|
|
1364
|
+
const v = part ? singleVersion(part, site, ctx) : null;
|
|
1365
|
+
if (!v) return null;
|
|
1366
|
+
out.push(...v);
|
|
1367
|
+
}
|
|
1368
|
+
return out;
|
|
1369
|
+
}
|
|
1370
|
+
function singleVersion(raw, site, ctx) {
|
|
1371
|
+
const spans = wordSpans(raw);
|
|
1372
|
+
const words = spans.map((s) => s.text);
|
|
1373
|
+
const v = headBy(raw, spans, words, site, ctx) ?? headScan(raw, words, site, ctx);
|
|
1374
|
+
const result = v ?? prefixForm(raw, words, site, ctx);
|
|
1375
|
+
return result ? [result] : null;
|
|
1376
|
+
}
|
|
1377
|
+
function baseVersion(raw, type, site) {
|
|
1378
|
+
return {
|
|
1379
|
+
type,
|
|
1380
|
+
raw,
|
|
1381
|
+
artists: [],
|
|
1382
|
+
modifiers: [],
|
|
1383
|
+
descriptor: null,
|
|
1384
|
+
year: null,
|
|
1385
|
+
unknownArtist: false,
|
|
1386
|
+
delimiter: site.delimiter,
|
|
1387
|
+
pos: site.pos,
|
|
1388
|
+
ambiguousMix: false,
|
|
1389
|
+
prefixForm: false
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
function remixerOpts() {
|
|
1393
|
+
return {
|
|
1394
|
+
role: "remixer",
|
|
1395
|
+
source: "version",
|
|
1396
|
+
lead: null,
|
|
1397
|
+
splitWith: false,
|
|
1398
|
+
featMarkersJoin: false
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
function dedupRemixers(credits) {
|
|
1402
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1403
|
+
return credits.filter((c) => {
|
|
1404
|
+
const k = dedupKey(c.name);
|
|
1405
|
+
if (seen.has(k)) return false;
|
|
1406
|
+
seen.add(k);
|
|
1407
|
+
return true;
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
function setCredits(v, text, site, ctx) {
|
|
1411
|
+
const r = listFromText(text, site.pos, ctx, remixerOpts());
|
|
1412
|
+
v.artists = dedupRemixers(r.credits);
|
|
1413
|
+
if (r.unknown) v.unknownArtist = true;
|
|
1414
|
+
const dropped = r.years[0];
|
|
1415
|
+
if (dropped) v.year ?? (v.year = dropped.year);
|
|
1416
|
+
}
|
|
1417
|
+
function headBy(raw, spans, words, site, ctx) {
|
|
1418
|
+
const head = ctx.t.heads.matchAt(words, 0);
|
|
1419
|
+
if (!head) return null;
|
|
1420
|
+
const by = words[head.length];
|
|
1421
|
+
const rest = spans[head.length + 1];
|
|
1422
|
+
if (by === void 0 || wordKey(by) !== "by" || rest === void 0) return null;
|
|
1423
|
+
const v = baseVersion(raw, head.entry.value, site);
|
|
1424
|
+
setCredits(v, raw.slice(rest.start), site, ctx);
|
|
1425
|
+
return v;
|
|
1426
|
+
}
|
|
1427
|
+
function collectRun(words, end, ctx) {
|
|
1428
|
+
const items = [];
|
|
1429
|
+
let j = end;
|
|
1430
|
+
while (j > 0) {
|
|
1431
|
+
const m = ctx.t.runWords.matchEnding(words, j);
|
|
1432
|
+
if (m) {
|
|
1433
|
+
items.unshift({ start: j - m.length, end: j, info: m.entry.value, year: null });
|
|
1434
|
+
j -= m.length;
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
const year = yearOf(words[j - 1]);
|
|
1438
|
+
if (year === null) break;
|
|
1439
|
+
items.unshift({ start: j - 1, end: j, info: {}, year });
|
|
1440
|
+
j--;
|
|
1441
|
+
}
|
|
1442
|
+
return items;
|
|
1443
|
+
}
|
|
1444
|
+
function headScan(raw, words, site, ctx) {
|
|
1445
|
+
const head = ctx.t.heads.matchEnding(words, words.length);
|
|
1446
|
+
if (!head) return null;
|
|
1447
|
+
const headType = head.entry.value;
|
|
1448
|
+
const headStart = words.length - head.length;
|
|
1449
|
+
const run2 = collectRun(words, headStart, ctx);
|
|
1450
|
+
const creditEnd = run2[0]?.start ?? headStart;
|
|
1451
|
+
const creditWords = words.slice(0, creditEnd);
|
|
1452
|
+
const left = run2[run2.length - 1];
|
|
1453
|
+
const generic = ctx.t.genericHeadTypes.has(headType);
|
|
1454
|
+
let type = headType;
|
|
1455
|
+
let typeItem = null;
|
|
1456
|
+
let viaMix = false;
|
|
1457
|
+
const nearestHead = generic ? nearestNonGenericHead(run2, ctx) : null;
|
|
1458
|
+
if (generic && left?.info.typeCapable) {
|
|
1459
|
+
type = left.info.typeCapable;
|
|
1460
|
+
typeItem = left;
|
|
1461
|
+
} else if (nearestHead?.info.head) {
|
|
1462
|
+
type = nearestHead.info.head;
|
|
1463
|
+
typeItem = nearestHead;
|
|
1464
|
+
} else if (headType === "mix" && creditWords.length > 0) {
|
|
1465
|
+
type = "remix";
|
|
1466
|
+
viaMix = true;
|
|
1467
|
+
}
|
|
1468
|
+
const v = baseVersion(raw, type, site);
|
|
1469
|
+
const descriptors = [];
|
|
1470
|
+
for (const item of run2) {
|
|
1471
|
+
const text = words.slice(item.start, item.end).join(" ");
|
|
1472
|
+
if (item.year !== null) {
|
|
1473
|
+
v.year ?? (v.year = item.year);
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
const modifier = item.info.typeCapable ?? item.info.descriptor;
|
|
1477
|
+
if (modifier !== void 0) {
|
|
1478
|
+
if (item !== typeItem && !v.modifiers.includes(modifier)) v.modifiers.push(modifier);
|
|
1479
|
+
} else if (item.info.genre) {
|
|
1480
|
+
descriptors.push(text);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
if (creditWords.length > 0) {
|
|
1484
|
+
const credit = creditWords.join(" ");
|
|
1485
|
+
const lastWord = asciiLower(creditWords[creditWords.length - 1]);
|
|
1486
|
+
if (isUnknownToken(credit, ctx.t)) {
|
|
1487
|
+
v.unknownArtist = true;
|
|
1488
|
+
} else if (type === "version" || type === "cover" || isAllJunkPhrases(creditWords, ctx)) {
|
|
1489
|
+
descriptors.unshift(credit);
|
|
1490
|
+
} else {
|
|
1491
|
+
const name = lastWord.endsWith("'s") ? credit.slice(0, -2) : credit;
|
|
1492
|
+
setCredits(v, name, site, ctx);
|
|
1493
|
+
const lastName = v.artists[v.artists.length - 1];
|
|
1494
|
+
if (viaMix && lastName && splitWords(lastName.name).length >= 3) {
|
|
1495
|
+
v.ambiguousMix = true;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
v.descriptor = descriptors.length > 0 ? descriptors.join(" ") : null;
|
|
1500
|
+
return v;
|
|
1501
|
+
}
|
|
1502
|
+
function nearestNonGenericHead(run2, ctx) {
|
|
1503
|
+
for (let i = run2.length - 1; i >= 0; i--) {
|
|
1504
|
+
const head = run2[i].info.head;
|
|
1505
|
+
if (head !== void 0 && !ctx.t.genericHeadTypes.has(head)) return run2[i];
|
|
1506
|
+
}
|
|
1507
|
+
return null;
|
|
1508
|
+
}
|
|
1509
|
+
function isAllJunkPhrases(words, ctx) {
|
|
1510
|
+
let i = 0;
|
|
1511
|
+
while (i < words.length) {
|
|
1512
|
+
const m = ctx.t.junkPhrases.matchAt(words, i);
|
|
1513
|
+
if (!m) return false;
|
|
1514
|
+
i += m.length;
|
|
1515
|
+
}
|
|
1516
|
+
return words.length > 0;
|
|
1517
|
+
}
|
|
1518
|
+
function prefixForm(raw, words, site, ctx) {
|
|
1519
|
+
const m = ctx.t.prefixForms.matchAt(words, 0);
|
|
1520
|
+
if (!m) return null;
|
|
1521
|
+
const v = baseVersion(raw, m.entry.value, site);
|
|
1522
|
+
v.prefixForm = true;
|
|
1523
|
+
const rest = [];
|
|
1524
|
+
for (const w of words.slice(m.length)) {
|
|
1525
|
+
const year = v.year === null ? yearOf(w) : null;
|
|
1526
|
+
if (year !== null) v.year = year;
|
|
1527
|
+
else rest.push(w);
|
|
1528
|
+
}
|
|
1529
|
+
v.descriptor = rest.length > 0 ? rest.join(" ") : null;
|
|
1530
|
+
return v;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// src/artists.ts
|
|
1534
|
+
function findFeatMarker(s, ctx, from = 0) {
|
|
1535
|
+
const spans = wordSpans(s.text);
|
|
1536
|
+
const words = spans.map((w) => w.text);
|
|
1537
|
+
for (let i = 0; i < spans.length; i++) {
|
|
1538
|
+
const span = spans[i];
|
|
1539
|
+
if (!span || span.start < from) continue;
|
|
1540
|
+
const before = s.text[span.start - 1];
|
|
1541
|
+
if (before !== " " && before !== PH) continue;
|
|
1542
|
+
const m = ctx.t.featMarkers.matchAt(words, i);
|
|
1543
|
+
if (!m) continue;
|
|
1544
|
+
const last = spans[i + m.length - 1];
|
|
1545
|
+
if (!last || s.text[last.end] !== " ") continue;
|
|
1546
|
+
return { start: span.start, end: last.end, marker: s.text.slice(span.start, last.end) };
|
|
1547
|
+
}
|
|
1548
|
+
return null;
|
|
1549
|
+
}
|
|
1550
|
+
function parseArtistSide(side, ctx) {
|
|
1551
|
+
const result = {
|
|
1552
|
+
credits: [],
|
|
1553
|
+
junk: [],
|
|
1554
|
+
years: [],
|
|
1555
|
+
unknownArtist: false,
|
|
1556
|
+
explicit: false,
|
|
1557
|
+
clean: false
|
|
1558
|
+
};
|
|
1559
|
+
const removed = /* @__PURE__ */ new Set();
|
|
1560
|
+
for (const { group } of groupsOf(side)) {
|
|
1561
|
+
const site = { source: "artist", delimiter: group.open, pos: group.pos };
|
|
1562
|
+
const c = classify(group.inner, site, ctx);
|
|
1563
|
+
if (c.kind === "feat" || c.kind === "producer") {
|
|
1564
|
+
result.credits.push(...c.credits);
|
|
1565
|
+
result.years.push(...c.years);
|
|
1566
|
+
if (c.unknown) result.unknownArtist = true;
|
|
1567
|
+
} else if (c.kind === "flag") {
|
|
1568
|
+
result[c.flag] = true;
|
|
1569
|
+
} else if (c.kind === "year") {
|
|
1570
|
+
result.years.push({ year: c.year, pos: group.pos });
|
|
1571
|
+
} else if (c.kind === "junk") {
|
|
1572
|
+
result.junk.push({ raw: group.inner, kind: c.junkKind, pos: group.pos });
|
|
1573
|
+
} else {
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1576
|
+
removed.add(group.pos);
|
|
1577
|
+
}
|
|
1578
|
+
const text = removeGroups(side, removed);
|
|
1579
|
+
const feat = findFeatMarker(text, ctx);
|
|
1580
|
+
const main = feat ? trimSkel(sliceSkel(text, 0, feat.start)) : trimSkel(text);
|
|
1581
|
+
const mainList = splitCredits(
|
|
1582
|
+
main,
|
|
1583
|
+
{ role: "primary", source: "artist", lead: null, splitWith: true, featMarkersJoin: false },
|
|
1584
|
+
ctx
|
|
1585
|
+
);
|
|
1586
|
+
result.credits.push(...mainList.credits);
|
|
1587
|
+
result.years.push(...mainList.years);
|
|
1588
|
+
if (mainList.unknown) result.unknownArtist = true;
|
|
1589
|
+
if (feat) {
|
|
1590
|
+
const featured = splitCredits(
|
|
1591
|
+
trimSkel(sliceSkel(text, feat.end)),
|
|
1592
|
+
{
|
|
1593
|
+
role: "featured",
|
|
1594
|
+
source: "artist",
|
|
1595
|
+
lead: feat.marker,
|
|
1596
|
+
splitWith: false,
|
|
1597
|
+
featMarkersJoin: true
|
|
1598
|
+
},
|
|
1599
|
+
ctx
|
|
1600
|
+
);
|
|
1601
|
+
result.credits.push(...featured.credits);
|
|
1602
|
+
result.years.push(...featured.years);
|
|
1603
|
+
if (featured.unknown) result.unknownArtist = true;
|
|
1604
|
+
}
|
|
1605
|
+
result.credits.sort((a, b) => a.pos - b.pos);
|
|
1606
|
+
return result;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// src/mode.ts
|
|
1610
|
+
function stripPlatformSuffixes(s, ctx, junk) {
|
|
1611
|
+
let str = s;
|
|
1612
|
+
let stripped = true;
|
|
1613
|
+
while (stripped) {
|
|
1614
|
+
stripped = false;
|
|
1615
|
+
const lower = asciiLower(str);
|
|
1616
|
+
for (const suffix of ctx.t.platformSuffixes) {
|
|
1617
|
+
if (!lower.endsWith(asciiLower(suffix))) continue;
|
|
1618
|
+
const start = str.length - suffix.length;
|
|
1619
|
+
junk.push({ raw: str.slice(start + 3), kind: "platform", pos: start + 3 });
|
|
1620
|
+
str = str.slice(0, start);
|
|
1621
|
+
stripped = true;
|
|
1622
|
+
break;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
return str;
|
|
1626
|
+
}
|
|
1627
|
+
function extensionOf(s, ctx) {
|
|
1628
|
+
const dot = s.lastIndexOf(".");
|
|
1629
|
+
if (dot < 0) return null;
|
|
1630
|
+
const ext = asciiLower(s.slice(dot + 1));
|
|
1631
|
+
return ctx.t.extensions.has(ext) ? ext : null;
|
|
1632
|
+
}
|
|
1633
|
+
function splitPipes(skel) {
|
|
1634
|
+
const t = skel.text;
|
|
1635
|
+
const cuts = [];
|
|
1636
|
+
for (let i = 1; i < t.length; i++) {
|
|
1637
|
+
if (t[i - 1] !== " " || t[i] !== "|") continue;
|
|
1638
|
+
const bars = t[i + 1] === "|" ? 2 : 1;
|
|
1639
|
+
if (t[i + bars] === " " && t[i + bars] !== void 0) {
|
|
1640
|
+
cuts.push([i - 1, i + bars + 1]);
|
|
1641
|
+
i += bars;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
const segments = [];
|
|
1645
|
+
let start = 0;
|
|
1646
|
+
for (const [a, b] of [...cuts, [t.length, t.length]]) {
|
|
1647
|
+
let s = start;
|
|
1648
|
+
let e = a;
|
|
1649
|
+
while (s < e && t[s] === " ") s++;
|
|
1650
|
+
while (e > s && t[e - 1] === " ") e--;
|
|
1651
|
+
segments.push({ skel: sliceSkel(skel, s, e), start: s, end: e });
|
|
1652
|
+
start = b;
|
|
1653
|
+
}
|
|
1654
|
+
return segments;
|
|
1655
|
+
}
|
|
1656
|
+
function trailingJunkPhrase(text, ctx) {
|
|
1657
|
+
const spans = wordSpans(text);
|
|
1658
|
+
const m = ctx.t.junkPhrases.matchEnding(
|
|
1659
|
+
spans.map((s) => s.text),
|
|
1660
|
+
spans.length
|
|
1661
|
+
);
|
|
1662
|
+
if (!m) return null;
|
|
1663
|
+
const first = spans[spans.length - m.length];
|
|
1664
|
+
return first ? { start: first.start, kind: m.entry.value } : null;
|
|
1665
|
+
}
|
|
1666
|
+
function isJunkText(text, ctx) {
|
|
1667
|
+
const words = splitWords(text);
|
|
1668
|
+
return words.length > 0 ? junkKindOf(words, ctx) : null;
|
|
1669
|
+
}
|
|
1670
|
+
function looksLikeYoutube(skel, ctx) {
|
|
1671
|
+
if (splitPipes(skel).length > 1) return true;
|
|
1672
|
+
for (const { group } of groupsOf(skel)) {
|
|
1673
|
+
const kind = isJunkText(group.inner, ctx);
|
|
1674
|
+
if (kind && kind !== "genre") return true;
|
|
1675
|
+
}
|
|
1676
|
+
return trailingJunkPhrase(skel.text, ctx) !== null;
|
|
1677
|
+
}
|
|
1678
|
+
function prelude(normalized, requested, ctx) {
|
|
1679
|
+
const junk = [];
|
|
1680
|
+
let str = stripPlatformSuffixes(normalized, ctx, junk);
|
|
1681
|
+
const hadPlatform = junk.length > 0;
|
|
1682
|
+
let mode;
|
|
1683
|
+
if (requested === "clean" || requested === "youtube" || requested === "filename") {
|
|
1684
|
+
mode = requested;
|
|
1685
|
+
} else if (extensionOf(str, ctx)) {
|
|
1686
|
+
mode = "filename";
|
|
1687
|
+
} else {
|
|
1688
|
+
mode = "clean";
|
|
1689
|
+
}
|
|
1690
|
+
if (mode === "filename") {
|
|
1691
|
+
if (extensionOf(str, ctx)) str = str.slice(0, str.lastIndexOf("."));
|
|
1692
|
+
str = str.trim();
|
|
1693
|
+
if (!str.includes(" ") && str.includes("_")) str = str.split("_").join(" ").trim();
|
|
1694
|
+
}
|
|
1695
|
+
const { skel, unbalanced } = scan(str, 0);
|
|
1696
|
+
if (unbalanced) warn(ctx, "unbalancedBrackets");
|
|
1697
|
+
if (requested === "auto" || requested === void 0) {
|
|
1698
|
+
if (mode === "clean" && (hadPlatform || looksLikeYoutube(skel, ctx))) mode = "youtube";
|
|
1699
|
+
}
|
|
1700
|
+
const whole = { text: str, base: 0 };
|
|
1701
|
+
if (mode !== "youtube") return { mode, working: whole, pipeTitle: null, junk };
|
|
1702
|
+
return { mode, ...splitYoutubePipes(skel, ctx, junk), junk };
|
|
1703
|
+
}
|
|
1704
|
+
function pieceOf(seg) {
|
|
1705
|
+
return { text: renderSkel(seg.skel), base: seg.skel.pos[0] ?? 0 };
|
|
1706
|
+
}
|
|
1707
|
+
function splitYoutubePipes(skel, ctx, junk) {
|
|
1708
|
+
const segments = splitPipes(skel);
|
|
1709
|
+
if (segments.length <= 1)
|
|
1710
|
+
return { working: { text: renderSkel(skel), base: 0 }, pipeTitle: null };
|
|
1711
|
+
const remaining = [];
|
|
1712
|
+
for (const seg of segments) {
|
|
1713
|
+
const text = collapseSpaces(renderSkel(seg.skel));
|
|
1714
|
+
const kind = hasSpacedDash(seg.skel.text) ? null : isJunkText(text, ctx);
|
|
1715
|
+
if (kind) junk.push({ raw: text, kind, pos: seg.skel.pos[0] ?? 0 });
|
|
1716
|
+
else if (text.length > 0) remaining.push(seg);
|
|
1717
|
+
}
|
|
1718
|
+
const pushOther = (keep, title = null) => {
|
|
1719
|
+
for (const seg of remaining) {
|
|
1720
|
+
if (seg === keep || seg === title) continue;
|
|
1721
|
+
junk.push({
|
|
1722
|
+
raw: collapseSpaces(renderSkel(seg.skel)),
|
|
1723
|
+
kind: "other",
|
|
1724
|
+
pos: seg.skel.pos[0] ?? 0
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
const withDash = remaining.find((seg) => hasSpacedDash(seg.skel.text));
|
|
1729
|
+
if (withDash) {
|
|
1730
|
+
pushOther(withDash);
|
|
1731
|
+
return { working: pieceOf(withDash), pipeTitle: null };
|
|
1732
|
+
}
|
|
1733
|
+
const [first, second] = remaining;
|
|
1734
|
+
if (remaining.length === 2 && first && second) {
|
|
1735
|
+
warn(ctx, "ambiguousSeparator");
|
|
1736
|
+
return { working: pieceOf(first), pipeTitle: pieceOf(second) };
|
|
1737
|
+
}
|
|
1738
|
+
if (first) {
|
|
1739
|
+
pushOther(first);
|
|
1740
|
+
return { working: pieceOf(first), pipeTitle: null };
|
|
1741
|
+
}
|
|
1742
|
+
return { working: { text: "", base: 0 }, pipeTitle: null };
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// src/prefix.ts
|
|
1746
|
+
function digitRun(s, from) {
|
|
1747
|
+
let i = from;
|
|
1748
|
+
while (isDigit(s[i])) i++;
|
|
1749
|
+
return i - from;
|
|
1750
|
+
}
|
|
1751
|
+
function twoDigitsUpTo59(s, at) {
|
|
1752
|
+
if (!isDigit(s[at]) || !isDigit(s[at + 1]) || isDigit(s[at + 2])) return null;
|
|
1753
|
+
const n = Number(s.slice(at, at + 2));
|
|
1754
|
+
return n <= 59 ? n : null;
|
|
1755
|
+
}
|
|
1756
|
+
function matchTime(s, at) {
|
|
1757
|
+
const lead = digitRun(s, at);
|
|
1758
|
+
if (lead < 1 || lead > 2 || s[at + lead] !== ":") return null;
|
|
1759
|
+
const first = Number(s.slice(at, at + lead));
|
|
1760
|
+
const mid = twoDigitsUpTo59(s, at + lead + 1);
|
|
1761
|
+
if (mid === null) return null;
|
|
1762
|
+
const afterMid = at + lead + 3;
|
|
1763
|
+
if (s[afterMid] === ":") {
|
|
1764
|
+
const sec = twoDigitsUpTo59(s, afterMid + 1);
|
|
1765
|
+
if (sec !== null) {
|
|
1766
|
+
const end = afterMid + 3;
|
|
1767
|
+
return { raw: s.slice(at, end), seconds: first * 3600 + mid * 60 + sec, end };
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
if (lead === 2 && first > 59) return null;
|
|
1771
|
+
return { raw: s.slice(at, afterMid), seconds: first * 60 + mid, end: afterMid };
|
|
1772
|
+
}
|
|
1773
|
+
function skipSpaceAndDash(s, at) {
|
|
1774
|
+
let i = at;
|
|
1775
|
+
if (s[i] === " ") i++;
|
|
1776
|
+
if (s[i] === "-" && s[i + 1] === " ") i += 2;
|
|
1777
|
+
return i;
|
|
1778
|
+
}
|
|
1779
|
+
function matchTimestamp(s) {
|
|
1780
|
+
const bracket = s[0] === "[" ? "]" : s[0] === "(" ? ")" : null;
|
|
1781
|
+
const t = matchTime(s, bracket ? 1 : 0);
|
|
1782
|
+
if (!t) return null;
|
|
1783
|
+
let end = t.end;
|
|
1784
|
+
if (bracket) {
|
|
1785
|
+
if (s[end] !== bracket) return null;
|
|
1786
|
+
end++;
|
|
1787
|
+
}
|
|
1788
|
+
if (end < s.length && s[end] !== " ") return null;
|
|
1789
|
+
return { ts: { raw: t.raw, seconds: t.seconds }, end: skipSpaceAndDash(s, end) };
|
|
1790
|
+
}
|
|
1791
|
+
function containsSpacedDash(s) {
|
|
1792
|
+
return hasSpacedDash(scan(s).skel.text);
|
|
1793
|
+
}
|
|
1794
|
+
function matchPosition(s, mode) {
|
|
1795
|
+
const n = digitRun(s, 0);
|
|
1796
|
+
if (n < 1 || n > 3) return null;
|
|
1797
|
+
const c = s[n];
|
|
1798
|
+
const zeroPadded = s[0] === "0" && n >= 2;
|
|
1799
|
+
if ((c === "." || c === ")") && (s[n + 1] === " " || n + 1 === s.length)) {
|
|
1800
|
+
return skipSpaceAndDash(s, n + 1);
|
|
1801
|
+
}
|
|
1802
|
+
if (zeroPadded && c === " ") return skipSpaceAndDash(s, n);
|
|
1803
|
+
if (c === " " && s[n + 1] === "-" && s[n + 2] === " ") {
|
|
1804
|
+
if (mode === "filename" || containsSpacedDash(s.slice(n + 3))) return n + 3;
|
|
1805
|
+
return null;
|
|
1806
|
+
}
|
|
1807
|
+
if (mode === "filename") {
|
|
1808
|
+
if ((c === "-" || c === "_" || c === ".") && s[n + 1] !== void 0 && s[n + 1] !== " ") {
|
|
1809
|
+
return n + 1;
|
|
1810
|
+
}
|
|
1811
|
+
if (c === " " && !containsSpacedDash(s.slice(n + 1))) return skipSpaceAndDash(s, n);
|
|
1812
|
+
}
|
|
1813
|
+
return null;
|
|
1814
|
+
}
|
|
1815
|
+
function stripPrefixes(str, base, mode, ctx) {
|
|
1816
|
+
let rest = str;
|
|
1817
|
+
let offset = base;
|
|
1818
|
+
let timestamp = null;
|
|
1819
|
+
let position = null;
|
|
1820
|
+
const ts = matchTimestamp(rest);
|
|
1821
|
+
if (ts) {
|
|
1822
|
+
timestamp = ts.ts;
|
|
1823
|
+
rest = rest.slice(ts.end);
|
|
1824
|
+
offset += ts.end;
|
|
1825
|
+
}
|
|
1826
|
+
if (matchKnownAt(rest, 0, ctx, "-") === 0) {
|
|
1827
|
+
const end = matchPosition(rest, mode);
|
|
1828
|
+
if (end !== null && end < rest.length) {
|
|
1829
|
+
const raw = rest.slice(0, digitRun(rest, 0));
|
|
1830
|
+
position = { raw, number: Number(raw) };
|
|
1831
|
+
rest = rest.slice(end);
|
|
1832
|
+
offset += end;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
return { rest, base: offset, timestamp, position };
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// src/separator.ts
|
|
1839
|
+
function classifySegment(seg, ctx) {
|
|
1840
|
+
if (hasGroup(seg)) return { kind: "unknown" };
|
|
1841
|
+
const text = collapseSpaces(seg.text);
|
|
1842
|
+
return classify(text, { source: "title", delimiter: "-", pos: seg.pos[0] ?? 0 }, ctx);
|
|
1843
|
+
}
|
|
1844
|
+
function isPeelable(c) {
|
|
1845
|
+
return c.kind === "junk" || c.kind === "flag" || c.kind === "year" || c.kind === "versions";
|
|
1846
|
+
}
|
|
1847
|
+
function peelSuffixes(segments, relaxed, ctx) {
|
|
1848
|
+
const remaining = [...segments];
|
|
1849
|
+
const peeled = [];
|
|
1850
|
+
while (remaining.length >= 2) {
|
|
1851
|
+
const last = remaining[remaining.length - 1];
|
|
1852
|
+
const cls = classifySegment(last.skel, ctx);
|
|
1853
|
+
if (!isPeelable(cls)) break;
|
|
1854
|
+
const words = splitWords(last.skel.text);
|
|
1855
|
+
const prefixForm2 = cls.kind === "versions" && cls.versions.every((v) => v.prefixForm);
|
|
1856
|
+
const ok = relaxed || remaining.length - 1 >= 2 || words.length >= 2 && !prefixForm2 || words.some((w) => yearOf(w) !== null) || cls.kind === "junk";
|
|
1857
|
+
if (!ok) break;
|
|
1858
|
+
peeled.unshift({ cls, raw: collapseSpaces(last.skel.text), pos: last.skel.pos[0] ?? 0 });
|
|
1859
|
+
remaining.pop();
|
|
1860
|
+
}
|
|
1861
|
+
return { remaining, peeled };
|
|
1862
|
+
}
|
|
1863
|
+
function nonEmpty(s) {
|
|
1864
|
+
const t = trimSkel(s);
|
|
1865
|
+
return t.text.length > 0 ? t : null;
|
|
1866
|
+
}
|
|
1867
|
+
function splitAt(s, cutStart, cutEnd) {
|
|
1868
|
+
return { artist: nonEmpty(sliceSkel(s, 0, cutStart)), title: trimSkel(sliceSkel(s, cutEnd)) };
|
|
1869
|
+
}
|
|
1870
|
+
function findSeparator(input, ctx) {
|
|
1871
|
+
const { skel, mode } = input;
|
|
1872
|
+
const segments = splitAtSpacedDashes(skel, true);
|
|
1873
|
+
const { remaining, peeled } = peelSuffixes(segments, false, ctx);
|
|
1874
|
+
if (remaining.length >= 2) {
|
|
1875
|
+
const first = remaining[0];
|
|
1876
|
+
const second = remaining[1];
|
|
1877
|
+
const lastSeg = remaining[remaining.length - 1];
|
|
1878
|
+
const artist2 = first.skel.text.length > 0 ? first.skel : null;
|
|
1879
|
+
const title = sliceSkel(skel, second.start, lastSeg.end);
|
|
1880
|
+
return { artist: artist2, title, split: artist2 !== null, peeled };
|
|
1881
|
+
}
|
|
1882
|
+
const rest = remaining[0].skel;
|
|
1883
|
+
const other = splitWithoutSpacedDash(rest, mode, ctx);
|
|
1884
|
+
if (other) return { ...other, split: other.artist !== null, peeled };
|
|
1885
|
+
const artist = mode === "youtube" ? input.uploader : null;
|
|
1886
|
+
if (rest.text.length > 0 && artist === null) warn(ctx, "noSeparator");
|
|
1887
|
+
return { artist, title: rest, split: false, peeled };
|
|
1888
|
+
}
|
|
1889
|
+
function splitWithoutSpacedDash(s, mode, ctx) {
|
|
1890
|
+
const asym = asymmetricDash(s.text);
|
|
1891
|
+
if (asym >= 0) {
|
|
1892
|
+
warn(ctx, "asymmetricDashSplit");
|
|
1893
|
+
return splitAt(s, asym, asym + 1);
|
|
1894
|
+
}
|
|
1895
|
+
if (mode === "youtube") {
|
|
1896
|
+
const quoted = quotedTitle(s);
|
|
1897
|
+
if (quoted) {
|
|
1898
|
+
warn(ctx, "quotedTitleSplit");
|
|
1899
|
+
return quoted;
|
|
1900
|
+
}
|
|
1901
|
+
const by = bySplit(s, ctx);
|
|
1902
|
+
if (by >= 0) {
|
|
1903
|
+
warn(ctx, "bySplit");
|
|
1904
|
+
const parts = splitAt(s, by, by + 4);
|
|
1905
|
+
return { artist: parts.title.text.length > 0 ? parts.title : null, title: parts.artist ?? s };
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
if (mode === "youtube" || mode === "filename") {
|
|
1909
|
+
const dash = unspacedDash(s, ctx);
|
|
1910
|
+
if (dash >= 0) {
|
|
1911
|
+
warn(ctx, "unspacedDashSplit");
|
|
1912
|
+
return splitAt(s, dash, dash + 1);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
return null;
|
|
1916
|
+
}
|
|
1917
|
+
function asymmetricDash(t) {
|
|
1918
|
+
for (let i = 1; i < t.length - 1; i++) {
|
|
1919
|
+
if (t[i] !== "-") continue;
|
|
1920
|
+
const left = isWhitespace(t[i - 1]);
|
|
1921
|
+
const right = isWhitespace(t[i + 1]);
|
|
1922
|
+
if (left !== right) return i;
|
|
1923
|
+
}
|
|
1924
|
+
return -1;
|
|
1925
|
+
}
|
|
1926
|
+
function quotedTitle(s) {
|
|
1927
|
+
const t = s.text;
|
|
1928
|
+
const open = t.indexOf('"');
|
|
1929
|
+
if (open <= 0) return null;
|
|
1930
|
+
const close = t.indexOf('"', open + 1);
|
|
1931
|
+
if (close < 0) return null;
|
|
1932
|
+
let artist = trimSkel(sliceSkel(s, 0, open));
|
|
1933
|
+
const last = artist.text[artist.text.length - 1];
|
|
1934
|
+
if (last === ":" || last === "-") artist = trimSkel(sliceSkel(artist, 0, artist.text.length - 1));
|
|
1935
|
+
const title = trimSkel(concatSkel(sliceSkel(s, open + 1, close), sliceSkel(s, close + 1)));
|
|
1936
|
+
return { artist: artist.text.length > 0 ? artist : null, title };
|
|
1937
|
+
}
|
|
1938
|
+
function bySplit(s, ctx) {
|
|
1939
|
+
const t = s.text;
|
|
1940
|
+
for (let i = t.lastIndexOf(" by "); i > 0; i = t.lastIndexOf(" by ", i - 1)) {
|
|
1941
|
+
const right = splitWords(t.slice(i + 4)).filter((w) => w !== PH);
|
|
1942
|
+
if (right.length === 0) continue;
|
|
1943
|
+
if (right.length === 1 && inVocab(right[0], ctx.t.stopwords)) continue;
|
|
1944
|
+
return i;
|
|
1945
|
+
}
|
|
1946
|
+
return -1;
|
|
1947
|
+
}
|
|
1948
|
+
function unspacedDash(s, ctx) {
|
|
1949
|
+
const t = s.text;
|
|
1950
|
+
let lastCandidate = -1;
|
|
1951
|
+
for (let i = 1; i < t.length - 1; i++) {
|
|
1952
|
+
if (t[i] !== "-" || isWhitespace(t[i - 1]) || isWhitespace(t[i + 1])) continue;
|
|
1953
|
+
if (!(t[i + 1] === '"' || isLetterAt(t, i + 1))) continue;
|
|
1954
|
+
if (ctx.knownKeys.size > 0 && isKnownArtist(renderSkel(sliceSkel(s, 0, i)), ctx)) return i;
|
|
1955
|
+
lastCandidate = i;
|
|
1956
|
+
}
|
|
1957
|
+
return lastCandidate;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// src/title.ts
|
|
1961
|
+
function emptyExtracted() {
|
|
1962
|
+
return {
|
|
1963
|
+
credits: [],
|
|
1964
|
+
versions: [],
|
|
1965
|
+
junk: [],
|
|
1966
|
+
years: [],
|
|
1967
|
+
flags: { explicit: false, clean: false, unknownArtist: false }
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
function addPeeled(out, peeled) {
|
|
1971
|
+
for (const p of peeled) {
|
|
1972
|
+
const c = p.cls;
|
|
1973
|
+
if (c.kind === "versions") out.versions.push(...c.versions);
|
|
1974
|
+
else if (c.kind === "junk") out.junk.push({ raw: p.raw, kind: c.junkKind, pos: p.pos });
|
|
1975
|
+
else if (c.kind === "flag") out.flags[c.flag] = true;
|
|
1976
|
+
else out.years.push({ year: c.year, pos: p.pos });
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
function parseTitleSide(input, out, ctx) {
|
|
1980
|
+
let skel = input.skel;
|
|
1981
|
+
const removed = extractGroups(skel, out, ctx);
|
|
1982
|
+
const segments = splitAtSpacedDashes(skel);
|
|
1983
|
+
const { remaining, peeled } = peelSuffixes(segments, input.relaxed, ctx);
|
|
1984
|
+
addPeeled(out, peeled);
|
|
1985
|
+
const lastKept = remaining[remaining.length - 1];
|
|
1986
|
+
skel = sliceSkel(skel, 0, lastKept.end);
|
|
1987
|
+
skel = extractUnbracketedCredits(skel, out, ctx);
|
|
1988
|
+
skel = removeGroups(skel, removed);
|
|
1989
|
+
if (input.youtube) skel = stripTrailingJunk(skel, out, ctx);
|
|
1990
|
+
const title = cleanupTitle(renderSkel(skel));
|
|
1991
|
+
return { title, unknownTitle: title.length > 0 && isUnknownToken(title, ctx.t) };
|
|
1992
|
+
}
|
|
1993
|
+
function extractGroups(skel, out, ctx) {
|
|
1994
|
+
const removed = /* @__PURE__ */ new Set();
|
|
1995
|
+
for (const { group } of groupsOf(skel)) {
|
|
1996
|
+
const site = { source: "title", delimiter: group.open, pos: group.pos };
|
|
1997
|
+
const c = classify(group.inner, site, ctx);
|
|
1998
|
+
switch (c.kind) {
|
|
1999
|
+
case "versions":
|
|
2000
|
+
out.versions.push(...c.versions);
|
|
2001
|
+
break;
|
|
2002
|
+
case "junk":
|
|
2003
|
+
out.junk.push({ raw: group.inner, kind: c.junkKind, pos: group.pos });
|
|
2004
|
+
break;
|
|
2005
|
+
case "flag":
|
|
2006
|
+
out.flags[c.flag] = true;
|
|
2007
|
+
break;
|
|
2008
|
+
case "year":
|
|
2009
|
+
out.years.push({ year: c.year, pos: group.pos });
|
|
2010
|
+
break;
|
|
2011
|
+
case "feat":
|
|
2012
|
+
case "producer":
|
|
2013
|
+
out.credits.push(...c.credits);
|
|
2014
|
+
out.years.push(...c.years);
|
|
2015
|
+
if (c.unknown) out.flags.unknownArtist = true;
|
|
2016
|
+
break;
|
|
2017
|
+
case "unknown":
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
removed.add(group.pos);
|
|
2021
|
+
}
|
|
2022
|
+
return removed;
|
|
2023
|
+
}
|
|
2024
|
+
function titleMarkers(s, ctx) {
|
|
2025
|
+
const spans = wordSpans(s.text);
|
|
2026
|
+
const words = spans.map((w) => w.text);
|
|
2027
|
+
const hits = [];
|
|
2028
|
+
for (let i = 0; i < spans.length; i++) {
|
|
2029
|
+
const span = spans[i];
|
|
2030
|
+
if (s.text[span.start - 1] !== " ") continue;
|
|
2031
|
+
let role = "featured";
|
|
2032
|
+
let lastIndex = -1;
|
|
2033
|
+
const feat = ctx.t.titleFeatMarkers.matchAt(words, i);
|
|
2034
|
+
if (feat) {
|
|
2035
|
+
lastIndex = i + feat.length - 1;
|
|
2036
|
+
} else {
|
|
2037
|
+
const prod = ctx.t.titleProducerMarkers.matchAt(words, i);
|
|
2038
|
+
if (!prod) continue;
|
|
2039
|
+
role = "producer";
|
|
2040
|
+
lastIndex = i + prod.length - 1;
|
|
2041
|
+
}
|
|
2042
|
+
const last = spans[lastIndex];
|
|
2043
|
+
if (s.text[last.end] !== " ") continue;
|
|
2044
|
+
hits.push({
|
|
2045
|
+
start: span.start,
|
|
2046
|
+
end: last.end,
|
|
2047
|
+
marker: s.text.slice(span.start, last.end),
|
|
2048
|
+
role
|
|
2049
|
+
});
|
|
2050
|
+
i = lastIndex;
|
|
2051
|
+
}
|
|
2052
|
+
return hits;
|
|
2053
|
+
}
|
|
2054
|
+
function extractUnbracketedCredits(input, out, ctx) {
|
|
2055
|
+
const markers = titleMarkers(input, ctx);
|
|
2056
|
+
const cuts = [];
|
|
2057
|
+
let i = 0;
|
|
2058
|
+
while (i < markers.length) {
|
|
2059
|
+
const hit = markers[i];
|
|
2060
|
+
let listEnd = input.text.indexOf(PH, hit.end);
|
|
2061
|
+
if (listEnd < 0) listEnd = input.text.length;
|
|
2062
|
+
const other = markers.find((m, k) => k > i && m.role !== hit.role && m.start >= hit.end);
|
|
2063
|
+
if (other && other.start < listEnd) listEnd = other.start;
|
|
2064
|
+
const list = trimSkel(sliceSkel(input, hit.end, listEnd));
|
|
2065
|
+
if (list.text.length > 0) {
|
|
2066
|
+
const r = splitCredits(
|
|
2067
|
+
list,
|
|
2068
|
+
{
|
|
2069
|
+
role: hit.role,
|
|
2070
|
+
source: "title",
|
|
2071
|
+
lead: hit.marker,
|
|
2072
|
+
splitWith: false,
|
|
2073
|
+
featMarkersJoin: hit.role === "featured"
|
|
2074
|
+
},
|
|
2075
|
+
ctx
|
|
2076
|
+
);
|
|
2077
|
+
out.credits.push(...r.credits);
|
|
2078
|
+
out.years.push(...r.years);
|
|
2079
|
+
if (r.unknown) out.flags.unknownArtist = true;
|
|
2080
|
+
cuts.push([hit.start, listEnd]);
|
|
2081
|
+
} else {
|
|
2082
|
+
listEnd = hit.end;
|
|
2083
|
+
}
|
|
2084
|
+
while (i < markers.length && markers[i].start < listEnd) i++;
|
|
2085
|
+
}
|
|
2086
|
+
let skel = input;
|
|
2087
|
+
for (const [start, end] of cuts.reverse()) skel = cutSkel(skel, start, end);
|
|
2088
|
+
return skel;
|
|
2089
|
+
}
|
|
2090
|
+
function stripTrailingJunk(input, out, ctx) {
|
|
2091
|
+
let skel = trimSkel(input);
|
|
2092
|
+
for (; ; ) {
|
|
2093
|
+
const hit = trailingJunkPhrase(skel.text, ctx);
|
|
2094
|
+
if (!hit) return skel;
|
|
2095
|
+
let before = trimSkel(sliceSkel(skel, 0, hit.start));
|
|
2096
|
+
const lastChar = before.text[before.text.length - 1];
|
|
2097
|
+
const lone = before.text.length === 1 || before.text[before.text.length - 2] === " ";
|
|
2098
|
+
if ((lastChar === "-" || lastChar === "|") && lone) {
|
|
2099
|
+
before = trimSkel(sliceSkel(before, 0, before.text.length - 1));
|
|
2100
|
+
}
|
|
2101
|
+
if (before.text.length === 0) return skel;
|
|
2102
|
+
const raw = collapseSpaces(skel.text.slice(hit.start));
|
|
2103
|
+
out.junk.push({ raw, kind: hit.kind, pos: skel.pos[hit.start] ?? 0 });
|
|
2104
|
+
skel = before;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
function isLoneEdge(word) {
|
|
2108
|
+
return word === "-" || word === "|";
|
|
2109
|
+
}
|
|
2110
|
+
function stripLoneEdges(s) {
|
|
2111
|
+
const words = s.split(" ");
|
|
2112
|
+
while (words.length > 0 && isLoneEdge(words[0])) words.shift();
|
|
2113
|
+
while (words.length > 0 && isLoneEdge(words[words.length - 1])) words.pop();
|
|
2114
|
+
return words.join(" ");
|
|
2115
|
+
}
|
|
2116
|
+
function cleanupTitle(raw) {
|
|
2117
|
+
let title = stripLoneEdges(collapseSpaces(raw));
|
|
2118
|
+
const q = title[0];
|
|
2119
|
+
if (title.length >= 2 && (q === '"' || q === "'") && title[title.length - 1] === q) {
|
|
2120
|
+
title = stripLoneEdges(collapseSpaces(title.slice(1, -1)));
|
|
2121
|
+
}
|
|
2122
|
+
return title;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// src/parser.ts
|
|
2126
|
+
var UPLOADER_BASE = -1e9;
|
|
2127
|
+
function emptyTrack(input, mode) {
|
|
2128
|
+
return {
|
|
2129
|
+
input,
|
|
2130
|
+
mode,
|
|
2131
|
+
position: null,
|
|
2132
|
+
timestamp: null,
|
|
2133
|
+
artists: [],
|
|
2134
|
+
title: "",
|
|
2135
|
+
fullTitle: "",
|
|
2136
|
+
versions: [],
|
|
2137
|
+
year: null,
|
|
2138
|
+
flags: { explicit: false, clean: false, unknownArtist: false, unknownTitle: false },
|
|
2139
|
+
junk: [],
|
|
2140
|
+
warnings: []
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
function skelOf(piece, ctx) {
|
|
2144
|
+
const r = scan(piece.text, piece.base);
|
|
2145
|
+
if (r.unbalanced) warn(ctx, "unbalancedBrackets");
|
|
2146
|
+
return trimSkel(r.skel);
|
|
2147
|
+
}
|
|
2148
|
+
function uploaderSkel(uploader) {
|
|
2149
|
+
if (typeof uploader !== "string") return null;
|
|
2150
|
+
let u = normalize(uploader);
|
|
2151
|
+
if (u.endsWith(" - Topic")) u = u.slice(0, -" - Topic".length);
|
|
2152
|
+
if (u.endsWith("VEVO")) u = u.slice(0, -"VEVO".length).trimEnd();
|
|
2153
|
+
if (u.length === 0) return null;
|
|
2154
|
+
return scan(u, UPLOADER_BASE).skel;
|
|
2155
|
+
}
|
|
2156
|
+
function run(input, options, t) {
|
|
2157
|
+
const ctx = makeCtx(t, options);
|
|
2158
|
+
const norm = normalize(input);
|
|
2159
|
+
const requested = options.mode ?? "auto";
|
|
2160
|
+
const pre = prelude(norm, requested, ctx);
|
|
2161
|
+
const track = emptyTrack(typeof input === "string" ? input : "", pre.mode);
|
|
2162
|
+
if (norm.length === 0) return track;
|
|
2163
|
+
const prefix = stripPrefixes(pre.working.text, pre.working.base, pre.mode, ctx);
|
|
2164
|
+
track.timestamp = prefix.timestamp;
|
|
2165
|
+
track.position = prefix.position;
|
|
2166
|
+
const working = skelOf({ text: prefix.rest, base: prefix.base }, ctx);
|
|
2167
|
+
const out = emptyExtracted();
|
|
2168
|
+
let artistSkel;
|
|
2169
|
+
let titleSkel;
|
|
2170
|
+
let relaxed;
|
|
2171
|
+
if (pre.pipeTitle) {
|
|
2172
|
+
artistSkel = working.text.length > 0 ? working : null;
|
|
2173
|
+
titleSkel = skelOf(pre.pipeTitle, ctx);
|
|
2174
|
+
relaxed = artistSkel !== null;
|
|
2175
|
+
} else {
|
|
2176
|
+
const sep = findSeparator(
|
|
2177
|
+
{ skel: working, mode: pre.mode, uploader: uploaderSkel(options.uploader) },
|
|
2178
|
+
ctx
|
|
2179
|
+
);
|
|
2180
|
+
addPeeled(out, sep.peeled);
|
|
2181
|
+
artistSkel = sep.artist;
|
|
2182
|
+
titleSkel = sep.title;
|
|
2183
|
+
relaxed = sep.split;
|
|
2184
|
+
}
|
|
2185
|
+
const artistSide = artistSkel ? parseArtistSide(artistSkel, ctx) : null;
|
|
2186
|
+
const titleSide = parseTitleSide(
|
|
2187
|
+
{ skel: titleSkel, relaxed, youtube: pre.mode === "youtube" },
|
|
2188
|
+
out,
|
|
2189
|
+
ctx
|
|
2190
|
+
);
|
|
2191
|
+
const versions = byPos(out.versions);
|
|
2192
|
+
for (const v of versions) if (v.ambiguousMix) warn(ctx, "ambiguousMixCredit");
|
|
2193
|
+
track.artists = dedupArtists([...artistSide?.credits ?? [], ...byPos(out.credits)]);
|
|
2194
|
+
track.title = titleSide.title;
|
|
2195
|
+
track.versions = versions.map(toVersion);
|
|
2196
|
+
track.fullTitle = fullTitleOf(track.title, track.versions);
|
|
2197
|
+
const years = byPos([...artistSide?.years ?? [], ...out.years]);
|
|
2198
|
+
track.year = years[0]?.year ?? null;
|
|
2199
|
+
track.flags = {
|
|
2200
|
+
explicit: out.flags.explicit || (artistSide?.explicit ?? false),
|
|
2201
|
+
clean: out.flags.clean || (artistSide?.clean ?? false),
|
|
2202
|
+
unknownArtist: (artistSide?.unknownArtist ?? false) || out.flags.unknownArtist,
|
|
2203
|
+
unknownTitle: titleSide.unknownTitle
|
|
2204
|
+
};
|
|
2205
|
+
track.junk = byPos([...pre.junk, ...artistSide?.junk ?? [], ...out.junk]).map((j) => ({
|
|
2206
|
+
raw: j.raw,
|
|
2207
|
+
kind: j.kind
|
|
2208
|
+
}));
|
|
2209
|
+
track.warnings = ctx.warnings;
|
|
2210
|
+
return track;
|
|
2211
|
+
}
|
|
2212
|
+
function runArtists(input, options, t) {
|
|
2213
|
+
const ctx = makeCtx(t, options);
|
|
2214
|
+
const norm = normalize(input);
|
|
2215
|
+
if (norm.length === 0) return [];
|
|
2216
|
+
const side = parseArtistSide(scan(norm, 0).skel, ctx);
|
|
2217
|
+
return dedupArtists(side.credits).map((a) => ({ ...a, source: "artist" }));
|
|
2218
|
+
}
|
|
2219
|
+
function safeOptions(options) {
|
|
2220
|
+
if (options === null || typeof options !== "object") return {};
|
|
2221
|
+
return Object.fromEntries(
|
|
2222
|
+
Object.entries(options).filter(([, v]) => v !== void 0)
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
function createParser(base = {}) {
|
|
2226
|
+
const baseOptions = safeOptions(base);
|
|
2227
|
+
const baseTables = tablesFor(baseOptions.keywords);
|
|
2228
|
+
const resolve2 = (options) => {
|
|
2229
|
+
const merged = { ...baseOptions, ...safeOptions(options) };
|
|
2230
|
+
const tables = options?.keywords ? tablesFor(mergeKeywords(baseOptions, options)) : baseTables;
|
|
2231
|
+
return { merged, tables };
|
|
2232
|
+
};
|
|
2233
|
+
return {
|
|
2234
|
+
parse(input, options) {
|
|
2235
|
+
const { merged, tables } = resolve2(options);
|
|
2236
|
+
return run(input, merged, tables);
|
|
2237
|
+
},
|
|
2238
|
+
parseArtists(input, options) {
|
|
2239
|
+
const { merged, tables } = resolve2(options);
|
|
2240
|
+
return runArtists(input, merged, tables);
|
|
2241
|
+
}
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
function mergeKeywords(a, b) {
|
|
2245
|
+
const x = a.keywords ?? {};
|
|
2246
|
+
const y = b.keywords ?? {};
|
|
2247
|
+
return {
|
|
2248
|
+
versionHeads: { ...x.versionHeads, ...y.versionHeads },
|
|
2249
|
+
descriptors: [...x.descriptors ?? [], ...y.descriptors ?? []],
|
|
2250
|
+
genres: [...x.genres ?? [], ...y.genres ?? []],
|
|
2251
|
+
junk: { ...x.junk, ...y.junk },
|
|
2252
|
+
featMarkers: [...x.featMarkers ?? [], ...y.featMarkers ?? []]
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
var defaultParser = createParser();
|
|
2256
|
+
function parse(input, options) {
|
|
2257
|
+
return defaultParser.parse(input, options);
|
|
2258
|
+
}
|
|
2259
|
+
function parseArtists(input, options) {
|
|
2260
|
+
return defaultParser.parseArtists(input, options);
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
exports.SPEC_VERSION = SPEC_VERSION;
|
|
2264
|
+
exports.allArtists = allArtists;
|
|
2265
|
+
exports.createParser = createParser;
|
|
2266
|
+
exports.format = format;
|
|
2267
|
+
exports.normalize = normalize;
|
|
2268
|
+
exports.parse = parse;
|
|
2269
|
+
exports.parseArtists = parseArtists;
|