@uimaxbai/am-lyrics 1.1.2 → 1.1.4

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.
@@ -85,6 +85,8 @@ export declare class AmLyrics extends LitElement {
85
85
  */
86
86
  private static fetchLyricsFromLrclib;
87
87
  private static fetchLyricsFromGenius;
88
+ private static calculateLineAlignments;
89
+ private static parseTTML;
88
90
  private static convertKPoeLyrics;
89
91
  private static toMilliseconds;
90
92
  firstUpdated(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"AmLyrics.d.ts","sourceRoot":"","sources":["../../src/AmLyrics.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AmLyrics.d.ts","sourceRoot":"","sources":["../../src/AmLyrics.ts"],"names":[],"mappings":"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"}
@@ -309,7 +309,7 @@ class GoogleService {
309
309
  }
310
310
  }
311
311
 
312
- const VERSION = '1.1.2';
312
+ const VERSION = '1.1.4';
313
313
  const INSTRUMENTAL_THRESHOLD_MS = 7000; // Show dots for gaps >= 7s
314
314
  const KPOE_SERVERS = [
315
315
  'https://lyricsplus.binimum.org',
@@ -851,6 +851,80 @@ class AmLyrics extends i {
851
851
  return 10;
852
852
  };
853
853
  const allResults = [];
854
+ // Try cache API first
855
+ try {
856
+ const cacheParams = new URLSearchParams({
857
+ track: title,
858
+ artist,
859
+ });
860
+ if (metadata.album) {
861
+ cacheParams.append('album', metadata.album);
862
+ }
863
+ if (metadata.durationMs && metadata.durationMs > 0) {
864
+ cacheParams.append('duration', Math.round(metadata.durationMs / 1000).toString());
865
+ }
866
+ const cacheUrl = `https://lyrics-api.binimum.org/?${cacheParams.toString()}`;
867
+ const cacheRes = await fetch(cacheUrl);
868
+ if (cacheRes.ok) {
869
+ const cacheData = await cacheRes.json();
870
+ if (cacheData.results && cacheData.results.length > 0) {
871
+ const result = cacheData.results[0];
872
+ if (result.timing_type === 'word' && result.lyricsUrl) {
873
+ const ttmlRes = await fetch(result.lyricsUrl);
874
+ if (ttmlRes.ok) {
875
+ const ttmlText = await ttmlRes.text();
876
+ const lines = AmLyrics.parseTTML(ttmlText);
877
+ if (lines && lines.length > 0) {
878
+ allResults.push({ lines, source: 'BiniLyrics' });
879
+ return allResults;
880
+ }
881
+ }
882
+ }
883
+ else {
884
+ // Not word type, try QQ
885
+ const qqParams = new URLSearchParams(params);
886
+ qqParams.set('source', 'qq');
887
+ const qqUrl = `https://lyricsplus.binimum.org/v2/lyrics/get?${qqParams.toString()}`;
888
+ try {
889
+ const qqRes = await fetch(qqUrl);
890
+ if (qqRes.ok) {
891
+ const payload = await qqRes.json();
892
+ const lines = AmLyrics.convertKPoeLyrics(payload);
893
+ const hasWordSync = lines?.some((line) => line.text &&
894
+ Array.isArray(line.text) &&
895
+ line.text.length > 1);
896
+ if (lines && lines.length > 0 && hasWordSync) {
897
+ allResults.push({ lines, source: 'QQ (Cache Fallback)' });
898
+ return allResults;
899
+ }
900
+ }
901
+ }
902
+ catch (qqError) {
903
+ // Ignore QQ fetch error
904
+ }
905
+ // If QQ fails or has no word sync, fall back to bini lyrics
906
+ if (result.lyricsUrl) {
907
+ const ttmlRes = await fetch(result.lyricsUrl);
908
+ if (ttmlRes.ok) {
909
+ const ttmlText = await ttmlRes.text();
910
+ const lines = AmLyrics.parseTTML(ttmlText);
911
+ if (lines && lines.length > 0) {
912
+ allResults.push({
913
+ lines,
914
+ source: 'Apple Music (Cache Fallback)',
915
+ });
916
+ return allResults;
917
+ }
918
+ }
919
+ }
920
+ }
921
+ }
922
+ }
923
+ }
924
+ catch (e) {
925
+ // eslint-disable-next-line no-console
926
+ console.error('Cache API failed', e);
927
+ }
854
928
  // Shuffle servers so we pick a random one first, with all others as fallback
855
929
  // Limit to 2 servers to prevent unnecessary API spam when Apple lyrics are missing
856
930
  const shuffledServers = [...KPOE_SERVERS]
@@ -930,6 +1004,7 @@ class AmLyrics extends i {
930
1004
  if (!match) {
931
1005
  // Skip non-timestamped lines (headers like [ti:], [ar:], etc.)
932
1006
  // eslint-disable-next-line no-continue
1007
+ // eslint-disable-next-line no-continue
933
1008
  continue;
934
1009
  }
935
1010
  const minutes = parseInt(match[1], 10);
@@ -949,6 +1024,7 @@ class AmLyrics extends i {
949
1024
  const endtime = i + 1 < parsed.length ? parsed[i + 1].timestamp : timestamp + 5000;
950
1025
  // Skip empty lines (instrumental gaps)
951
1026
  if (!text.trim()) {
1027
+ // eslint-disable-next-line no-continue
952
1028
  // eslint-disable-next-line no-continue
953
1029
  continue;
954
1030
  }
@@ -992,6 +1068,7 @@ class AmLyrics extends i {
992
1068
  // eslint-disable-next-line no-await-in-loop
993
1069
  const searchResponse = await fetch(`${normalizedBase}/search/?${searchParams.toString()}`);
994
1070
  if (!searchResponse.ok) {
1071
+ // eslint-disable-next-line no-continue
995
1072
  // eslint-disable-next-line no-continue
996
1073
  continue;
997
1074
  }
@@ -999,6 +1076,7 @@ class AmLyrics extends i {
999
1076
  const searchData = await searchResponse.json();
1000
1077
  const items = searchData?.data?.items;
1001
1078
  if (!Array.isArray(items) || items.length === 0) {
1079
+ // eslint-disable-next-line no-continue
1002
1080
  // eslint-disable-next-line no-continue
1003
1081
  continue;
1004
1082
  }
@@ -1012,6 +1090,7 @@ class AmLyrics extends i {
1012
1090
  }
1013
1091
  const trackId = bestTrack?.id;
1014
1092
  if (!trackId) {
1093
+ // eslint-disable-next-line no-continue
1015
1094
  // eslint-disable-next-line no-continue
1016
1095
  continue;
1017
1096
  }
@@ -1019,6 +1098,7 @@ class AmLyrics extends i {
1019
1098
  // eslint-disable-next-line no-await-in-loop
1020
1099
  const lyricsResponse = await fetch(`${normalizedBase}/lyrics/?id=${trackId}`);
1021
1100
  if (!lyricsResponse.ok) {
1101
+ // eslint-disable-next-line no-continue
1022
1102
  // eslint-disable-next-line no-continue
1023
1103
  continue;
1024
1104
  }
@@ -1143,10 +1223,230 @@ class AmLyrics extends i {
1143
1223
  }
1144
1224
  }
1145
1225
  catch {
1226
+ // eslint-disable-next-line no-console
1146
1227
  console.error('No Genius lyrics found');
1147
1228
  }
1148
1229
  return null;
1149
1230
  }
1231
+ static calculateLineAlignments(lineSingers, agentTypes) {
1232
+ const lineSideAssignments = new Array(lineSingers.length).fill(undefined);
1233
+ let currentSideIsLeft = true;
1234
+ let lastPersonSingerId = null;
1235
+ let rightCount = 0;
1236
+ let totalCount = 0;
1237
+ lineSingers.forEach((singerId, index) => {
1238
+ let sideClass;
1239
+ if (singerId) {
1240
+ let type = agentTypes[singerId];
1241
+ if (!type) {
1242
+ if (singerId === 'v1000') {
1243
+ type = 'group';
1244
+ }
1245
+ else if (singerId === 'v2000') {
1246
+ type = 'other';
1247
+ }
1248
+ else {
1249
+ type = 'person';
1250
+ }
1251
+ }
1252
+ if (type === 'group') {
1253
+ sideClass = 'start';
1254
+ }
1255
+ else {
1256
+ if (lastPersonSingerId === null) {
1257
+ if (type === 'other') {
1258
+ currentSideIsLeft = false;
1259
+ }
1260
+ else {
1261
+ currentSideIsLeft = true;
1262
+ }
1263
+ }
1264
+ else if (singerId !== lastPersonSingerId) {
1265
+ currentSideIsLeft = !currentSideIsLeft;
1266
+ }
1267
+ sideClass = currentSideIsLeft ? 'start' : 'end';
1268
+ lastPersonSingerId = singerId;
1269
+ }
1270
+ }
1271
+ if (sideClass) {
1272
+ totalCount += 1;
1273
+ if (sideClass === 'end')
1274
+ rightCount += 1;
1275
+ }
1276
+ lineSideAssignments[index] = sideClass;
1277
+ });
1278
+ if (totalCount > 0 && Math.round((rightCount / totalCount) * 100) >= 85) {
1279
+ const flip = (s) => {
1280
+ if (s === 'start')
1281
+ return 'end';
1282
+ if (s === 'end')
1283
+ return 'start';
1284
+ return s;
1285
+ };
1286
+ for (let i = 0; i < lineSideAssignments.length; i += 1) {
1287
+ lineSideAssignments[i] = flip(lineSideAssignments[i]);
1288
+ }
1289
+ }
1290
+ return lineSideAssignments;
1291
+ }
1292
+ static parseTTML(ttmlString) {
1293
+ try {
1294
+ const parser = new DOMParser();
1295
+ const doc = parser.parseFromString(ttmlString, 'text/xml');
1296
+ const translations = {};
1297
+ const transliterations = {};
1298
+ const agentMap = {};
1299
+ const agents = doc.getElementsByTagName('ttm:agent');
1300
+ for (let i = 0; i < agents.length; i += 1) {
1301
+ const agent = agents[i];
1302
+ const id = agent.getAttribute('xml:id');
1303
+ const type = agent.getAttribute('type');
1304
+ if (id && type) {
1305
+ agentMap[id] = type;
1306
+ }
1307
+ }
1308
+ const translationNodes = doc.getElementsByTagName('translation');
1309
+ for (let i = 0; i < translationNodes.length; i += 1) {
1310
+ const texts = translationNodes[i].getElementsByTagName('text');
1311
+ for (let j = 0; j < texts.length; j += 1) {
1312
+ const textNode = texts[j];
1313
+ const key = textNode.getAttribute('for');
1314
+ if (key && textNode.textContent) {
1315
+ translations[key] = textNode.textContent;
1316
+ }
1317
+ }
1318
+ }
1319
+ const transliterationNodes = doc.getElementsByTagName('transliteration');
1320
+ for (let i = 0; i < transliterationNodes.length; i += 1) {
1321
+ const texts = transliterationNodes[i].getElementsByTagName('text');
1322
+ for (let j = 0; j < texts.length; j += 1) {
1323
+ const textNode = texts[j];
1324
+ const key = textNode.getAttribute('for');
1325
+ if (key && textNode.textContent) {
1326
+ transliterations[key] = textNode.textContent
1327
+ .trim()
1328
+ .replace(/\s+/g, ' ');
1329
+ }
1330
+ }
1331
+ }
1332
+ const timeToMs = (timeStr) => {
1333
+ if (!timeStr)
1334
+ return 0;
1335
+ const parts = timeStr.split(':');
1336
+ let seconds = 0;
1337
+ if (parts.length === 2) {
1338
+ seconds = parseInt(parts[0], 10) * 60 + parseFloat(parts[1]);
1339
+ }
1340
+ else if (parts.length === 3) {
1341
+ seconds =
1342
+ parseInt(parts[0], 10) * 3600 +
1343
+ parseInt(parts[1], 10) * 60 +
1344
+ parseFloat(parts[2]);
1345
+ }
1346
+ else {
1347
+ seconds = parseFloat(parts[0]);
1348
+ }
1349
+ return Math.round(seconds * 1000);
1350
+ };
1351
+ const lines = [];
1352
+ const pNodes = doc.getElementsByTagName('p');
1353
+ const lineSingers = [];
1354
+ for (let i = 0; i < pNodes.length; i += 1) {
1355
+ lineSingers.push(pNodes[i].getAttribute('ttm:agent') || undefined);
1356
+ }
1357
+ const alignments = AmLyrics.calculateLineAlignments(lineSingers, agentMap);
1358
+ for (let i = 0; i < pNodes.length; i += 1) {
1359
+ const p = pNodes[i];
1360
+ const key = p.getAttribute('itunes:key');
1361
+ const beginMs = timeToMs(p.getAttribute('begin'));
1362
+ const endMs = timeToMs(p.getAttribute('end'));
1363
+ let songPart;
1364
+ if (p.parentNode && p.parentNode.tagName === 'div') {
1365
+ songPart =
1366
+ p.parentNode.getAttribute('itunes:songPart') ||
1367
+ undefined;
1368
+ }
1369
+ const mainSyllables = [];
1370
+ const bgSyllables = [];
1371
+ const spans = p.getElementsByTagName('span');
1372
+ if (spans.length > 0) {
1373
+ for (let j = 0; j < spans.length; j += 1) {
1374
+ const span = spans[j];
1375
+ if (span.getAttribute('ttm:role') === 'x-bg') {
1376
+ const bgInnerSpans = span.getElementsByTagName('span');
1377
+ for (let k = 0; k < bgInnerSpans.length; k += 1) {
1378
+ const bgSpan = bgInnerSpans[k];
1379
+ let bgText = bgSpan.textContent || '';
1380
+ const nextNode = bgSpan.nextSibling;
1381
+ if (nextNode &&
1382
+ nextNode.nodeType === 3 &&
1383
+ /^\s/.test(nextNode.textContent || '') &&
1384
+ !bgText.endsWith(' ')) {
1385
+ bgText += ' ';
1386
+ }
1387
+ bgSyllables.push({
1388
+ text: bgText,
1389
+ timestamp: timeToMs(bgSpan.getAttribute('begin')),
1390
+ endtime: timeToMs(bgSpan.getAttribute('end')),
1391
+ part: false,
1392
+ });
1393
+ }
1394
+ // eslint-disable-next-line no-continue
1395
+ continue;
1396
+ }
1397
+ if (span.parentNode &&
1398
+ span.parentNode.getAttribute?.('ttm:role') === 'x-bg') {
1399
+ // eslint-disable-next-line no-continue
1400
+ continue;
1401
+ }
1402
+ let text = span.textContent || '';
1403
+ const nextNode = span.nextSibling;
1404
+ if (nextNode &&
1405
+ nextNode.nodeType === 3 &&
1406
+ /^\s/.test(nextNode.textContent || '') &&
1407
+ !text.endsWith(' ')) {
1408
+ text += ' ';
1409
+ }
1410
+ mainSyllables.push({
1411
+ text,
1412
+ timestamp: timeToMs(span.getAttribute('begin')),
1413
+ endtime: timeToMs(span.getAttribute('end')),
1414
+ part: false,
1415
+ });
1416
+ }
1417
+ }
1418
+ else {
1419
+ mainSyllables.push({
1420
+ text: p.textContent?.trim() || '',
1421
+ timestamp: beginMs,
1422
+ endtime: endMs,
1423
+ part: false,
1424
+ lineSynced: true,
1425
+ });
1426
+ }
1427
+ const alignment = alignments[i];
1428
+ lines.push({
1429
+ text: mainSyllables,
1430
+ background: bgSyllables.length > 0,
1431
+ backgroundText: bgSyllables,
1432
+ timestamp: beginMs,
1433
+ endtime: endMs,
1434
+ isWordSynced: spans.length > 0,
1435
+ alignment,
1436
+ songPart,
1437
+ translation: key ? translations[key] : undefined,
1438
+ romanizedText: key ? transliterations[key] : undefined,
1439
+ oppositeTurn: alignment === 'end',
1440
+ });
1441
+ }
1442
+ return lines;
1443
+ }
1444
+ catch (e) {
1445
+ // eslint-disable-next-line no-console
1446
+ console.error('Failed to parse TTML', e);
1447
+ return null;
1448
+ }
1449
+ }
1150
1450
  static convertKPoeLyrics(payload) {
1151
1451
  if (!payload) {
1152
1452
  return null;
@@ -1168,45 +1468,21 @@ class AmLyrics extends i {
1168
1468
  const lines = [];
1169
1469
  // If type is 'Line', we revert to line-by-line highlighting by skipping syllabus parsing
1170
1470
  const isLineType = payload.type === 'Line' || payload.type === 'line';
1171
- // Convert metadata.agents to alignment map
1172
- const agents = payload.metadata?.agents ?? {};
1173
- const agentEntries = Object.entries(agents);
1174
- const singerAlignmentMap = {};
1175
- if (agentEntries.length > 0) {
1176
- agentEntries.sort((a, b) => a[0].localeCompare(b[0]));
1177
- const personAgents = agentEntries.filter(
1178
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1179
- ([_, agentData]) => agentData.type === 'person');
1180
- const personIndexMap = new Map();
1181
- personAgents.forEach(([agentKey], personIndex) => {
1182
- personIndexMap.set(agentKey, personIndex);
1183
- });
1184
- agentEntries.forEach(([agentKey, agentData]) => {
1185
- const mappedKey = agentData.alias || agentKey;
1186
- if (agentData.type === 'group') {
1187
- singerAlignmentMap[mappedKey] = 'start';
1188
- }
1189
- else if (agentData.type === 'other') {
1190
- singerAlignmentMap[mappedKey] = 'end';
1191
- }
1192
- else if (agentData.type === 'person') {
1193
- const personIndex = personIndexMap.get(agentKey);
1194
- if (personIndex !== undefined) {
1195
- singerAlignmentMap[mappedKey] =
1196
- personIndex % 2 === 0 ? 'start' : 'end';
1197
- }
1198
- }
1471
+ // Convert metadata.agents to type map
1472
+ const agentTypes = {};
1473
+ if (payload.metadata?.agents) {
1474
+ Object.entries(payload.metadata.agents).forEach(([key, agent]) => {
1475
+ const mappedKey = agent.alias || key;
1476
+ agentTypes[mappedKey] = agent.type;
1199
1477
  });
1200
1478
  }
1201
- for (const entry of sanitizedEntries) {
1479
+ const lineSingers = sanitizedEntries.map((entry) => entry.element?.singer);
1480
+ const alignments = AmLyrics.calculateLineAlignments(lineSingers, agentTypes);
1481
+ for (let i = 0; i < sanitizedEntries.length; i += 1) {
1482
+ const entry = sanitizedEntries[i];
1202
1483
  const start = AmLyrics.toMilliseconds(entry.time);
1203
1484
  const duration = AmLyrics.toMilliseconds(entry.duration);
1204
- // Determine alignment
1205
- let alignment;
1206
- const singerId = entry.element?.singer;
1207
- if (singerId && singerAlignmentMap[singerId]) {
1208
- alignment = singerAlignmentMap[singerId];
1209
- }
1485
+ const alignment = alignments[i];
1210
1486
  const lineText = typeof entry.text === 'string' ? entry.text : '';
1211
1487
  const lineStart = AmLyrics.toMilliseconds(entry.time);
1212
1488
  const lineDuration = AmLyrics.toMilliseconds(entry.duration);
@@ -1257,10 +1533,8 @@ class AmLyrics extends i {
1257
1533
  // If syllabus data matches, map it to main syllables
1258
1534
  if (Array.isArray(transliteration.syllabus) &&
1259
1535
  transliteration.syllabus.length === mainSyllables.length) {
1260
- transliteration.syllabus.forEach((s, i) => {
1261
- if (mainSyllables[i]) {
1262
- mainSyllables[i].romanizedText = s.text;
1263
- }
1536
+ transliteration.syllabus.forEach((s, idx) => {
1537
+ mainSyllables[idx].romanizedText = s.text;
1264
1538
  });
1265
1539
  }
1266
1540
  }
@@ -1270,10 +1544,11 @@ class AmLyrics extends i {
1270
1544
  text: mainSyllables,
1271
1545
  background: backgroundSyllables.length > 0,
1272
1546
  backgroundText: backgroundSyllables,
1273
- oppositeTurn: Array.isArray(entry.element)
1274
- ? entry.element.includes('opposite') ||
1275
- entry.element.includes('right')
1276
- : false,
1547
+ oppositeTurn: alignment === 'end' ||
1548
+ (Array.isArray(entry.element)
1549
+ ? entry.element.includes('opposite') ||
1550
+ entry.element.includes('right')
1551
+ : false),
1277
1552
  timestamp: lineStart,
1278
1553
  endtime: start + duration,
1279
1554
  isWordSynced: isLineType ? false : hasWordSync,