@0xsharon/synco 0.0.0-stage → 1.0.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/dist/cli.cjs ADDED
@@ -0,0 +1,984 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/cli.ts
32
+ var cli_exports = {};
33
+ __export(cli_exports, {
34
+ runCli: () => runCli
35
+ });
36
+ module.exports = __toCommonJS(cli_exports);
37
+ var import_commander = require("commander");
38
+
39
+ // src/lyrics.ts
40
+ var import_node_fs = __toESM(require("fs"));
41
+ var import_node_path2 = __toESM(require("path"));
42
+
43
+ // src/types.ts
44
+ var TargetType = /* @__PURE__ */ ((TargetType2) => {
45
+ TargetType2["PLAINTEXT"] = "PLAINTEXT";
46
+ TargetType2["PREFER_SYNCED"] = "PREFER_SYNCED";
47
+ TargetType2["SYNCED_ONLY"] = "SYNCED_ONLY";
48
+ return TargetType2;
49
+ })(TargetType || {});
50
+
51
+ // src/utils.ts
52
+ var import_node_os = __toESM(require("os"));
53
+ var import_node_path = __toESM(require("path"));
54
+ var R_FEAT = /\((feat.+)\)/i;
55
+ function syncedToPlaintext(syncedLyrics) {
56
+ if (!syncedLyrics) return "";
57
+ return syncedLyrics.replace(/\[\d+:\d+(?:\.\d+)?\]\s*/g, "").replace(/<\d+:\d+(?:\.\d+)?>\s*/g, "").trim();
58
+ }
59
+ function identifyLyricsType(lrc) {
60
+ if (!lrc || typeof lrc !== "string" || !lrc.trim()) {
61
+ return "invalid";
62
+ }
63
+ const lines = lrc.split("\n").map((l) => l.trim()).filter(Boolean);
64
+ if (lines.length === 0) return "invalid";
65
+ const timeTagRegex = /\[\d{1,2}:\d{2}(?:\.\d{1,3})?\]/;
66
+ const taggedLines = lines.filter((line) => timeTagRegex.test(line));
67
+ if (taggedLines.length >= 2 || taggedLines.length / lines.length >= 0.25) {
68
+ return "synced";
69
+ }
70
+ return "plaintext";
71
+ }
72
+ function formatTime(timeInSeconds) {
73
+ const totalSeconds = Math.max(0, timeInSeconds);
74
+ let minutes = Math.floor(totalSeconds / 60);
75
+ let seconds = Math.floor(totalSeconds % 60);
76
+ let hundredths = Math.round((totalSeconds - Math.floor(totalSeconds)) * 100);
77
+ if (hundredths >= 100) {
78
+ hundredths = 0;
79
+ seconds += 1;
80
+ if (seconds >= 60) {
81
+ seconds = 0;
82
+ minutes += 1;
83
+ }
84
+ }
85
+ return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${String(hundredths).padStart(2, "0")}`;
86
+ }
87
+ function defaultProcess(str) {
88
+ return str.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").trim();
89
+ }
90
+ function indelDistance(s1, s2) {
91
+ const m = s1.length;
92
+ const n = s2.length;
93
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
94
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
95
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
96
+ for (let i = 1; i <= m; i++) {
97
+ for (let j = 1; j <= n; j++) {
98
+ if (s1[i - 1] === s2[j - 1]) {
99
+ dp[i][j] = dp[i - 1][j - 1];
100
+ } else {
101
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
102
+ }
103
+ }
104
+ }
105
+ return dp[m][n];
106
+ }
107
+ function fuzzRatio(s1, s2) {
108
+ if (!s1 && !s2) return 100;
109
+ if (!s1 || !s2) return 0;
110
+ const dist = indelDistance(s1, s2);
111
+ const totalLen = s1.length + s2.length;
112
+ return totalLen === 0 ? 100 : (totalLen - dist) / totalLen * 100;
113
+ }
114
+ function tokenSetRatio(s1, s2) {
115
+ const p1 = defaultProcess(s1);
116
+ const p2 = defaultProcess(s2);
117
+ if (!p1 && !p2) return 100;
118
+ if (!p1 || !p2) return 0;
119
+ const t1 = Array.from(new Set(p1.split(/\s+/).filter(Boolean)));
120
+ const t2 = Array.from(new Set(p2.split(/\s+/).filter(Boolean)));
121
+ const set1 = new Set(t1);
122
+ const set2 = new Set(t2);
123
+ const intersection = t1.filter((x) => set2.has(x)).sort();
124
+ const diff1 = t1.filter((x) => !set2.has(x)).sort();
125
+ const diff2 = t2.filter((x) => !set1.has(x)).sort();
126
+ const s0 = intersection.join(" ");
127
+ const s1Rest = diff1.join(" ");
128
+ const s2Rest = diff2.join(" ");
129
+ const combined1 = [s0, s1Rest].filter(Boolean).join(" ");
130
+ const combined2 = [s0, s2Rest].filter(Boolean).join(" ");
131
+ if (!s0) {
132
+ return fuzzRatio(combined1, combined2);
133
+ }
134
+ return Math.max(
135
+ fuzzRatio(s0, combined1),
136
+ fuzzRatio(s0, combined2),
137
+ fuzzRatio(combined1, combined2)
138
+ );
139
+ }
140
+ function strScore(a, b) {
141
+ let lowerA = (a || "").toLowerCase();
142
+ let lowerB = (b || "").toLowerCase();
143
+ if (!lowerB.includes("feat")) {
144
+ lowerA = lowerA.replace(R_FEAT, "");
145
+ lowerB = lowerB.replace(R_FEAT, "");
146
+ }
147
+ return tokenSetRatio(lowerA, lowerB);
148
+ }
149
+ function strSame(a, b, minScore = 65) {
150
+ return Math.round(strScore(a, b)) >= minScore;
151
+ }
152
+ function sortResults(results, searchTerm, compareKey) {
153
+ return [...results].sort((a, b) => {
154
+ const scoreA = strScore(compareKey(a), searchTerm);
155
+ const scoreB = strScore(compareKey(b), searchTerm);
156
+ return scoreB - scoreA;
157
+ });
158
+ }
159
+ function getBestMatch(results, searchTerm, compareKey, minScore = 65) {
160
+ if (!results || results.length === 0) return null;
161
+ const sorted = sortResults(results, searchTerm, compareKey);
162
+ const bestMatch = sorted[0];
163
+ const valueToCompare = compareKey(bestMatch);
164
+ if (!strSame(valueToCompare, searchTerm, minScore)) {
165
+ return null;
166
+ }
167
+ return bestMatch;
168
+ }
169
+ function getCachePath(libName = "synco") {
170
+ const platform = process.platform;
171
+ let baseDir;
172
+ if (platform === "win32") {
173
+ baseDir = process.env.LOCALAPPDATA || import_node_path.default.join(import_node_os.default.homedir(), "AppData", "Local");
174
+ } else if (platform === "darwin") {
175
+ baseDir = import_node_path.default.join(import_node_os.default.homedir(), "Library", "Caches");
176
+ } else {
177
+ baseDir = process.env.XDG_CACHE_HOME || import_node_path.default.join(import_node_os.default.homedir(), ".cache");
178
+ }
179
+ return import_node_path.default.join(baseDir, libName);
180
+ }
181
+ var Logger = class {
182
+ constructor(context, verbose = false) {
183
+ this.context = context;
184
+ this.verbose = verbose;
185
+ }
186
+ context;
187
+ verbose;
188
+ debug(...args) {
189
+ if (this.verbose) {
190
+ console.error(`[${this.context}] [DEBUG]`, ...args);
191
+ }
192
+ }
193
+ info(...args) {
194
+ if (this.verbose) {
195
+ console.error(`[${this.context}] [INFO]`, ...args);
196
+ }
197
+ }
198
+ warn(...args) {
199
+ console.error(`[${this.context}] [WARN]`, ...args);
200
+ }
201
+ error(...args) {
202
+ console.error(`[${this.context}] [ERROR]`, ...args);
203
+ }
204
+ };
205
+
206
+ // src/lyrics.ts
207
+ var Lyrics = class {
208
+ synced = null;
209
+ unsynced = null;
210
+ constructor(synced, unsynced) {
211
+ this.synced = synced ?? null;
212
+ this.unsynced = unsynced ?? null;
213
+ }
214
+ addUnknown(unknown) {
215
+ if (!unknown || typeof unknown !== "string") return;
216
+ const trimmed = unknown.trim();
217
+ if (!trimmed) return;
218
+ const type = identifyLyricsType(trimmed);
219
+ if (type === "synced") {
220
+ this.synced = trimmed;
221
+ } else if (type === "plaintext") {
222
+ this.unsynced = trimmed;
223
+ }
224
+ }
225
+ update(other) {
226
+ if (!other) return;
227
+ if (other.synced) {
228
+ this.synced = other.synced;
229
+ }
230
+ if (other.unsynced) {
231
+ this.unsynced = other.unsynced;
232
+ }
233
+ }
234
+ isPreferred(targetType) {
235
+ return Boolean(
236
+ this.synced || targetType === "PLAINTEXT" /* PLAINTEXT */ && this.unsynced
237
+ );
238
+ }
239
+ isAcceptable(targetType) {
240
+ return Boolean(
241
+ this.synced || targetType !== "SYNCED_ONLY" /* SYNCED_ONLY */ && this.unsynced
242
+ );
243
+ }
244
+ toStr(targetType) {
245
+ if (targetType === "PLAINTEXT" /* PLAINTEXT */) {
246
+ if (this.unsynced) return this.unsynced;
247
+ if (this.synced) return syncedToPlaintext(this.synced);
248
+ return null;
249
+ }
250
+ if (targetType === "PREFER_SYNCED" /* PREFER_SYNCED */) {
251
+ return this.synced || this.unsynced || null;
252
+ }
253
+ return this.synced || null;
254
+ }
255
+ async saveLrcFile(filePath, targetType) {
256
+ const text = this.toStr(targetType);
257
+ if (!text) return;
258
+ const resolvedPath = import_node_path2.default.resolve(filePath);
259
+ await import_node_fs.default.promises.mkdir(import_node_path2.default.dirname(resolvedPath), { recursive: true });
260
+ await import_node_fs.default.promises.writeFile(resolvedPath, text, "utf-8");
261
+ }
262
+ };
263
+
264
+ // src/providers/base.ts
265
+ var import_axios = __toESM(require("axios"));
266
+ var LRCProvider = class {
267
+ session;
268
+ logger;
269
+ options;
270
+ constructor(options = {}) {
271
+ this.options = options;
272
+ this.logger = new Logger(this.constructor.name, Boolean(options.verbose));
273
+ this.session = import_axios.default.create({
274
+ timeout: options.timeoutMs ?? 1e4,
275
+ headers: {
276
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 synco/1.0.0",
277
+ Accept: "application/json, text/plain, */*"
278
+ }
279
+ });
280
+ }
281
+ toString() {
282
+ return this.name;
283
+ }
284
+ async get(url, config) {
285
+ try {
286
+ const response = await this.session.get(url, config);
287
+ return response.data;
288
+ } catch (err) {
289
+ this.logger.debug(`GET request failed for ${url}:`, err?.message ?? err);
290
+ return null;
291
+ }
292
+ }
293
+ async post(url, data, config) {
294
+ try {
295
+ const response = await this.session.post(url, data, config);
296
+ return response.data;
297
+ } catch (err) {
298
+ this.logger.debug(`POST request failed for ${url}:`, err?.message ?? err);
299
+ return null;
300
+ }
301
+ }
302
+ };
303
+
304
+ // src/providers/lrclib.ts
305
+ var Lrclib = class _Lrclib extends LRCProvider {
306
+ name = "Lrclib";
307
+ static ROOT_URL = "https://lrclib.net";
308
+ static API_ENDPOINT = `${_Lrclib.ROOT_URL}/api`;
309
+ static SEARCH_ENDPOINT = `${_Lrclib.API_ENDPOINT}/search`;
310
+ static LRC_ENDPOINT = `${_Lrclib.API_ENDPOINT}/get/`;
311
+ constructor(options = {}) {
312
+ super(options);
313
+ }
314
+ async getLrcById(trackId) {
315
+ const url = `${_Lrclib.LRC_ENDPOINT}${trackId}`;
316
+ const data = await this.get(url);
317
+ if (!data) return null;
318
+ const lyrics = new Lyrics();
319
+ lyrics.synced = data.syncedLyrics || null;
320
+ lyrics.unsynced = data.plainLyrics || null;
321
+ return lyrics;
322
+ }
323
+ async getLrc(searchTerm) {
324
+ const data = await this.get(_Lrclib.SEARCH_ENDPOINT, {
325
+ params: { q: searchTerm }
326
+ });
327
+ if (!data || !Array.isArray(data) || data.length === 0) {
328
+ this.logger.debug(`No tracks found on Lrclib for "${searchTerm}"`);
329
+ return null;
330
+ }
331
+ const sorted = sortResults(
332
+ data,
333
+ searchTerm,
334
+ (t) => `${t.artistName} - ${t.trackName}`
335
+ );
336
+ const best = sorted[0];
337
+ if (!best) return null;
338
+ if (best.syncedLyrics || best.plainLyrics) {
339
+ return new Lyrics(best.syncedLyrics || null, best.plainLyrics || null);
340
+ }
341
+ return this.getLrcById(best.id);
342
+ }
343
+ };
344
+
345
+ // src/providers/megalobiz.ts
346
+ var cheerio = __toESM(require("cheerio"));
347
+ var Megalobiz = class _Megalobiz extends LRCProvider {
348
+ name = "Megalobiz";
349
+ static ROOT_URL = "https://www.megalobiz.com";
350
+ static API_ENDPOINT = "https://api.megalobiz.com/api/public/v1";
351
+ constructor(options = {}) {
352
+ super(options);
353
+ }
354
+ async getLrc(searchTerm) {
355
+ const apiResult = await this.getViaApi(searchTerm);
356
+ if (apiResult) return apiResult;
357
+ return this.getViaWeb(searchTerm);
358
+ }
359
+ async getViaApi(searchTerm) {
360
+ try {
361
+ const data = await this.get(
362
+ `${_Megalobiz.API_ENDPOINT}/search`,
363
+ {
364
+ params: { q: searchTerm },
365
+ headers: {
366
+ "User-Agent": "synco-node/1.0 (https://github.com/0xSHARON/synco)"
367
+ }
368
+ }
369
+ );
370
+ const items = data?.results;
371
+ if (!items || !Array.isArray(items) || items.length === 0) {
372
+ return null;
373
+ }
374
+ const match = getBestMatch(
375
+ items,
376
+ searchTerm,
377
+ (t) => `${t.artist} ${t.title}`,
378
+ 60
379
+ );
380
+ const chosen = match || items[0];
381
+ const lrcUrl = chosen.lrc_url || `${_Megalobiz.API_ENDPOINT}/lrc/${chosen.id}?format=lrc`;
382
+ const lrcResponse = await this.session.get(lrcUrl, {
383
+ headers: {
384
+ "User-Agent": "synco-node/1.0 (https://github.com/0xSHARON/synco)"
385
+ },
386
+ responseType: "text"
387
+ });
388
+ if (lrcResponse.status === 200 && lrcResponse.data) {
389
+ const lyrics = new Lyrics();
390
+ lyrics.addUnknown(lrcResponse.data);
391
+ return lyrics;
392
+ }
393
+ } catch (err) {
394
+ this.logger.debug("Megalobiz API error:", err?.message ?? err);
395
+ }
396
+ return null;
397
+ }
398
+ async getViaWeb(searchTerm) {
399
+ try {
400
+ const query = searchTerm.replace(/\s+/g, "+");
401
+ const searchUrl = `${_Megalobiz.ROOT_URL}/search/all?qry=${query}&searchButton.x=0&searchButton.y=0`;
402
+ const html = await this.get(searchUrl, { responseType: "text" });
403
+ if (!html || typeof html !== "string") return null;
404
+ const $ = cheerio.load(html);
405
+ const links = [];
406
+ $('a[href^="/lrc/maker/"]').each((_, el) => {
407
+ const href = $(el).attr("href");
408
+ if (href) {
409
+ const rawText = $(el).text().replace(/by/gi, "").trim();
410
+ const words = rawText.split(/\s+/).slice(0, searchTerm.split(/\s+/).length + 1);
411
+ links.push({ href, text: words.join(" ") });
412
+ }
413
+ });
414
+ if (links.length === 0) return null;
415
+ const match = getBestMatch(links, searchTerm, (item) => item.text, 55);
416
+ if (!match) return null;
417
+ const lrcPageUrl = `${_Megalobiz.ROOT_URL}${match.href}`;
418
+ const pageHtml = await this.get(lrcPageUrl, { responseType: "text" });
419
+ if (!pageHtml) return null;
420
+ const lrcId = match.href.split(".").pop();
421
+ const page$ = cheerio.load(pageHtml);
422
+ const lrcText = page$(`div#lrc_${lrcId}_details`).text();
423
+ if (lrcText) {
424
+ const lyrics = new Lyrics();
425
+ lyrics.addUnknown(lrcText);
426
+ return lyrics;
427
+ }
428
+ } catch (err) {
429
+ this.logger.debug("Megalobiz web scraper error:", err?.message ?? err);
430
+ }
431
+ return null;
432
+ }
433
+ };
434
+
435
+ // src/providers/musixmatch.ts
436
+ var import_node_fs2 = __toESM(require("fs"));
437
+ var import_node_path3 = __toESM(require("path"));
438
+ var Musixmatch = class _Musixmatch extends LRCProvider {
439
+ name = "Musixmatch";
440
+ static ROOT_URL = "https://apic-desktop.musixmatch.com/ws/1.1/";
441
+ token = null;
442
+ lang;
443
+ enhanced;
444
+ constructor(options = {}) {
445
+ super(options);
446
+ this.lang = options.lang;
447
+ this.enhanced = Boolean(options.enhanced);
448
+ if (options.musixmatchToken || process.env.MUSIXMATCH_TOKEN) {
449
+ this.token = options.musixmatchToken || process.env.MUSIXMATCH_TOKEN || null;
450
+ }
451
+ }
452
+ async requestApi(action, params = {}) {
453
+ if (action !== "token.get" && !this.token) {
454
+ await this.ensureToken();
455
+ }
456
+ const query = {
457
+ app_id: "web-desktop-app-v1.0",
458
+ t: Date.now(),
459
+ ...params
460
+ };
461
+ if (this.token) {
462
+ query.usertoken = this.token;
463
+ }
464
+ const url = `${_Musixmatch.ROOT_URL}${action}`;
465
+ return this.get(url, { params: query });
466
+ }
467
+ async ensureToken() {
468
+ const tokenFile = import_node_path3.default.join(getCachePath("synco"), "musixmatch_token.json");
469
+ const currentTime = Math.floor(Date.now() / 1e3);
470
+ try {
471
+ if (import_node_fs2.default.existsSync(tokenFile)) {
472
+ const raw = import_node_fs2.default.readFileSync(tokenFile, "utf-8");
473
+ const cached = JSON.parse(raw);
474
+ if (cached.token && cached.token !== "00000000000000000000000000000000000000000000000000000000" && cached.expiration_time && currentTime < cached.expiration_time) {
475
+ this.token = cached.token;
476
+ return;
477
+ }
478
+ }
479
+ } catch {
480
+ }
481
+ const res = await this.requestApi("token.get", { user_language: "en" });
482
+ const statusCode = res?.message?.header?.status_code;
483
+ if (statusCode === 401) {
484
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
485
+ return this.ensureToken();
486
+ }
487
+ const newToken = res?.message?.body?.user_token;
488
+ if (newToken && typeof newToken === "string") {
489
+ this.token = newToken;
490
+ const tokenData = {
491
+ token: newToken,
492
+ expiration_time: currentTime + 600
493
+ // 10 minutes expiration
494
+ };
495
+ try {
496
+ import_node_fs2.default.mkdirSync(import_node_path3.default.dirname(tokenFile), { recursive: true });
497
+ import_node_fs2.default.writeFileSync(tokenFile, JSON.stringify(tokenData, null, 2), "utf-8");
498
+ } catch {
499
+ }
500
+ }
501
+ }
502
+ async getLrcById(trackId) {
503
+ const subtitleRes = await this.requestApi("track.subtitle.get", {
504
+ track_id: trackId,
505
+ subtitle_format: "lrc"
506
+ });
507
+ let translationList = [];
508
+ if (this.lang) {
509
+ const translationRes = await this.requestApi("crowd.track.translations.get", {
510
+ track_id: trackId,
511
+ subtitle_format: "lrc",
512
+ translation_fields_set: "minimal",
513
+ selected_language: this.lang
514
+ });
515
+ translationList = translationRes?.message?.body?.translations_list || [];
516
+ if (translationList.length === 0) {
517
+ this.logger.debug(`Could not find translations in ${this.lang} for track ${trackId}`);
518
+ }
519
+ }
520
+ const body = subtitleRes?.message?.body;
521
+ let lrcStr = body?.subtitle?.subtitle_body;
522
+ if (!lrcStr || typeof lrcStr !== "string") {
523
+ return null;
524
+ }
525
+ if (this.lang && translationList.length > 0) {
526
+ for (const item of translationList) {
527
+ const org = item?.translation?.subtitle_matched_line;
528
+ const tr = item?.translation?.description;
529
+ if (org && tr) {
530
+ lrcStr = lrcStr.replace(org, `${org}
531
+ (${tr})`);
532
+ }
533
+ }
534
+ }
535
+ const lyrics = new Lyrics();
536
+ lyrics.synced = lrcStr;
537
+ return lyrics;
538
+ }
539
+ async getLrcWordByWord(trackId) {
540
+ const res = await this.requestApi("track.richsync.get", {
541
+ track_id: trackId
542
+ });
543
+ if (res?.message?.header?.status_code !== 200) {
544
+ return null;
545
+ }
546
+ const rawRichsync = res?.message?.body?.richsync?.richsync_body;
547
+ if (!rawRichsync) return null;
548
+ try {
549
+ const items = typeof rawRichsync === "string" ? JSON.parse(rawRichsync) : rawRichsync;
550
+ let lrcStr = "";
551
+ for (const item of items) {
552
+ const baseTs = parseFloat(item.ts);
553
+ lrcStr += `[${formatTime(baseTs)}] `;
554
+ if (Array.isArray(item.l)) {
555
+ for (const line of item.l) {
556
+ const wordOffset = parseFloat(line.o);
557
+ const wordTime = formatTime(baseTs + wordOffset);
558
+ lrcStr += `<${wordTime}> ${line.c} `;
559
+ }
560
+ }
561
+ lrcStr += "\n";
562
+ }
563
+ if (lrcStr.trim()) {
564
+ const lyrics = new Lyrics();
565
+ lyrics.synced = lrcStr;
566
+ return lyrics;
567
+ }
568
+ } catch (err) {
569
+ this.logger.debug("Failed to parse Musixmatch word-by-word JSON:", err?.message ?? err);
570
+ }
571
+ return null;
572
+ }
573
+ async getLrc(searchTerm) {
574
+ const res = await this.requestApi("track.search", {
575
+ q: searchTerm,
576
+ page_size: 5,
577
+ page: 1
578
+ });
579
+ const statusCode = res?.message?.header?.status_code;
580
+ if (statusCode !== 200) {
581
+ this.logger.debug(`Musixmatch track.search returned status code ${statusCode}`);
582
+ return null;
583
+ }
584
+ const tracks = res?.message?.body?.track_list;
585
+ if (!tracks || !Array.isArray(tracks) || tracks.length === 0) {
586
+ return null;
587
+ }
588
+ const match = getBestMatch(
589
+ tracks,
590
+ searchTerm,
591
+ (t) => `${t.track?.track_name} ${t.track?.artist_name}`
592
+ );
593
+ if (!match?.track?.track_id) {
594
+ return null;
595
+ }
596
+ const trackId = match.track.track_id;
597
+ if (this.enhanced) {
598
+ const enhancedLyrics = await this.getLrcWordByWord(trackId);
599
+ if (enhancedLyrics && enhancedLyrics.synced) {
600
+ return enhancedLyrics;
601
+ }
602
+ }
603
+ return this.getLrcById(trackId);
604
+ }
605
+ };
606
+
607
+ // src/providers/netease.ts
608
+ var NetEase = class _NetEase extends LRCProvider {
609
+ name = "NetEase";
610
+ static API_ENDPOINT_METADATA = "https://music.163.com/api/search/pc";
611
+ static API_ENDPOINT_LYRICS = "https://music.163.com/api/song/lyric";
612
+ constructor(options = {}) {
613
+ super(options);
614
+ this.session.defaults.headers.common.Cookie = "NMTID=00OAVK3xqDG726ITU6jopU6jF2yMk0AAAGCO8l1BA; JSESSIONID-WYYY=8KQo11YK2GZP45RMlz8Kn80vHZ9%2FGvwzRKQXXy0iQoFKycWdBlQjbfT0MJrFa6hwRfmpfBYKeHliUPH287JC3hNW99WQjrh9b9RmKT%2Fg1Exc2VwHZcsqi7ITxQgfEiee50po28x5xTTZXKoP%2FRMctN2jpDeg57kdZrXz%2FD%2FWghb%5C4DuZ%3A1659124633932; _iuqxldmzr_=32; _ntes_nnid=0db6667097883aa9596ecfe7f188c3ec,1659122833973; _ntes_nuid=0db6667097883aa9596ecfe7f188c3ec;";
615
+ this.session.defaults.headers.common.Referer = "https://music.163.com/";
616
+ }
617
+ async searchTrack(searchTerm) {
618
+ const data = await this.get(_NetEase.API_ENDPOINT_METADATA, {
619
+ params: {
620
+ limit: 10,
621
+ type: 1,
622
+ offset: 0,
623
+ s: searchTerm
624
+ }
625
+ });
626
+ const songs = data?.result?.songs;
627
+ if (!songs || !Array.isArray(songs) || songs.length === 0) {
628
+ return null;
629
+ }
630
+ const match = getBestMatch(
631
+ songs,
632
+ searchTerm,
633
+ (t) => `${t.name} ${t.artists?.[0]?.name ?? ""}`
634
+ );
635
+ return match || songs[0];
636
+ }
637
+ async getLrcById(trackId) {
638
+ const data = await this.get(_NetEase.API_ENDPOINT_LYRICS, {
639
+ params: {
640
+ id: trackId,
641
+ lv: 1
642
+ }
643
+ });
644
+ const lyricText = data?.lrc?.lyric;
645
+ if (!lyricText || typeof lyricText !== "string") {
646
+ return null;
647
+ }
648
+ const lyrics = new Lyrics();
649
+ lyrics.addUnknown(lyricText);
650
+ return lyrics;
651
+ }
652
+ async getLrc(searchTerm) {
653
+ const track = await this.searchTrack(searchTerm);
654
+ if (!track?.id) return null;
655
+ return this.getLrcById(track.id);
656
+ }
657
+ };
658
+
659
+ // src/providers/genius.ts
660
+ var cheerio2 = __toESM(require("cheerio"));
661
+ var Genius = class _Genius extends LRCProvider {
662
+ name = "Genius";
663
+ static SEARCH_ENDPOINT = "https://genius.com/api/search/multi";
664
+ constructor(options = {}) {
665
+ super(options);
666
+ this.session.defaults.headers.common.Cookie = "obuid=e3ee67e0-7df9-4181-8324-d977c6dc9250;";
667
+ this.session.defaults.headers.common.Referer = "https://genius.com/";
668
+ }
669
+ async getLrc(searchTerm) {
670
+ try {
671
+ const data = await this.get(_Genius.SEARCH_ENDPOINT, {
672
+ params: {
673
+ per_page: 5,
674
+ q: searchTerm
675
+ }
676
+ });
677
+ const sections = data?.response?.sections;
678
+ if (!sections || !Array.isArray(sections)) return null;
679
+ const songSection = sections.find((s) => s.type === "song") || sections[1] || sections[0];
680
+ const hits = songSection?.hits;
681
+ if (!hits || !Array.isArray(hits) || hits.length === 0) return null;
682
+ const songUrl = hits[0]?.result?.url;
683
+ if (!songUrl) return null;
684
+ const pageHtml = await this.get(songUrl, {
685
+ responseType: "text"
686
+ });
687
+ if (!pageHtml || typeof pageHtml !== "string") return null;
688
+ const $ = cheerio2.load(pageHtml);
689
+ const containers = $('div[data-lyrics-container="true"]');
690
+ if (containers.length === 0) return null;
691
+ let lrcStr = "";
692
+ containers.each((_, el) => {
693
+ $(el).find("br").replaceWith("\n");
694
+ const text = $(el).text().trim();
695
+ if (text) {
696
+ lrcStr += `${text}
697
+
698
+ `;
699
+ }
700
+ });
701
+ const cleaned = lrcStr.trim();
702
+ if (!cleaned) return null;
703
+ const lyrics = new Lyrics();
704
+ lyrics.unsynced = cleaned;
705
+ return lyrics;
706
+ } catch (err) {
707
+ this.logger.debug("Genius provider error:", err?.message ?? err);
708
+ return null;
709
+ }
710
+ }
711
+ };
712
+
713
+ // src/providers/deezer.ts
714
+ var Deezer = class _Deezer extends LRCProvider {
715
+ name = "Deezer";
716
+ static SEARCH_ENDPOINT = "https://api.deezer.com/search";
717
+ static API_ENDPOINT = "https://www.deezer.com/ajax/gw-light.php";
718
+ token = "null";
719
+ constructor(options = {}) {
720
+ super(options);
721
+ }
722
+ async initializeToken() {
723
+ if (this.token !== "null") return;
724
+ try {
725
+ const data = await this.apiCall("deezer.getUserData");
726
+ const checkForm = data?.results?.checkForm;
727
+ if (checkForm) {
728
+ this.token = checkForm;
729
+ }
730
+ } catch {
731
+ }
732
+ }
733
+ async apiCall(method, jsonData) {
734
+ return this.post(
735
+ _Deezer.API_ENDPOINT,
736
+ jsonData,
737
+ {
738
+ params: {
739
+ api_version: "1.0",
740
+ api_token: this.token,
741
+ input: "3",
742
+ method
743
+ }
744
+ }
745
+ );
746
+ }
747
+ async getLrcById(trackId) {
748
+ await this.initializeToken();
749
+ const res = await this.apiCall("song.getLyrics", { sng_id: trackId });
750
+ const results = res?.results;
751
+ if (!results) return null;
752
+ const lyrics = new Lyrics();
753
+ const syncJson = results.LYRICS_SYNC_JSON;
754
+ if (!syncJson || !Array.isArray(syncJson)) {
755
+ lyrics.unsynced = results.LYRICS_TEXT || null;
756
+ return lyrics.unsynced ? lyrics : null;
757
+ }
758
+ let lrcStr = "";
759
+ for (const chunk of syncJson) {
760
+ if (chunk?.lrc_timestamp && chunk?.line) {
761
+ lrcStr += `${chunk.lrc_timestamp} ${chunk.line}
762
+ `;
763
+ }
764
+ }
765
+ lyrics.synced = lrcStr.trim() ? lrcStr : null;
766
+ return lyrics;
767
+ }
768
+ async getLrc(searchTerm) {
769
+ const data = await this.get(_Deezer.SEARCH_ENDPOINT, {
770
+ params: { q: searchTerm }
771
+ });
772
+ const tracks = data?.data;
773
+ if (!tracks || !Array.isArray(tracks) || tracks.length === 0) {
774
+ return null;
775
+ }
776
+ const match = getBestMatch(
777
+ tracks,
778
+ searchTerm,
779
+ (t) => `${t.title} ${t.artist?.name ?? ""}`
780
+ );
781
+ if (!match?.id) return null;
782
+ return this.getLrcById(match.id);
783
+ }
784
+ };
785
+
786
+ // src/providers/lyricsify.ts
787
+ var cheerio3 = __toESM(require("cheerio"));
788
+ var Lyricsify = class _Lyricsify extends LRCProvider {
789
+ name = "Lyricsify";
790
+ static ROOT_URL = "https://www.lyricsify.com";
791
+ static SEARCH_ENDPOINT = `${_Lyricsify.ROOT_URL}/search`;
792
+ constructor(options = {}) {
793
+ super(options);
794
+ }
795
+ async getLrc(searchTerm) {
796
+ try {
797
+ const html = await this.get(_Lyricsify.SEARCH_ENDPOINT, {
798
+ params: { q: searchTerm },
799
+ responseType: "text"
800
+ });
801
+ if (!html || typeof html !== "string") return null;
802
+ const $ = cheerio3.load(html);
803
+ const links = [];
804
+ $('a[href^="/lyric/"]').each((_, el) => {
805
+ const href = $(el).attr("href");
806
+ if (href) {
807
+ links.push({
808
+ href,
809
+ text: $(el).text().toLowerCase().replace(/-/g, " ").trim()
810
+ });
811
+ }
812
+ });
813
+ if (links.length === 0) return null;
814
+ const match = getBestMatch(links, searchTerm, (item) => item.text, 55);
815
+ if (!match) return null;
816
+ const lrcId = match.href.split(".").pop();
817
+ const pageHtml = await this.get(`${_Lyricsify.ROOT_URL}${match.href}`, {
818
+ responseType: "text"
819
+ });
820
+ if (!pageHtml) return null;
821
+ const page$ = cheerio3.load(pageHtml);
822
+ const lrcText = page$(`div#lyrics_${lrcId}_details`).text();
823
+ if (lrcText) {
824
+ const lyrics = new Lyrics();
825
+ lyrics.addUnknown(lrcText);
826
+ return lyrics;
827
+ }
828
+ } catch (err) {
829
+ this.logger.debug("Lyricsify provider error:", err?.message ?? err);
830
+ }
831
+ return null;
832
+ }
833
+ };
834
+
835
+ // src/index.ts
836
+ async function search(searchTerm, optOrPlainOnly, syncedOnly, savePath, providers, lang, enhanced) {
837
+ const options = typeof optOrPlainOnly === "object" && optOrPlainOnly !== null ? optOrPlainOnly : {
838
+ plainOnly: Boolean(optOrPlainOnly),
839
+ syncedOnly: Boolean(syncedOnly),
840
+ savePath,
841
+ providers,
842
+ lang,
843
+ enhanced: Boolean(enhanced)
844
+ };
845
+ const logger = new Logger("synco", Boolean(options.verbose));
846
+ if (options.plainOnly && options.syncedOnly) {
847
+ logger.error("--plain-only and --synced-only flags cannot be used together.");
848
+ return null;
849
+ }
850
+ let targetType = "PREFER_SYNCED" /* PREFER_SYNCED */;
851
+ if (options.plainOnly) {
852
+ targetType = "PLAINTEXT" /* PLAINTEXT */;
853
+ } else if (options.syncedOnly) {
854
+ targetType = "SYNCED_ONLY" /* SYNCED_ONLY */;
855
+ }
856
+ const lyrics = new Lyrics();
857
+ const providerInstances = [
858
+ new Musixmatch({
859
+ lang: options.lang,
860
+ enhanced: options.enhanced,
861
+ musixmatchToken: options.musixmatchToken,
862
+ timeoutMs: options.timeoutMs,
863
+ verbose: options.verbose
864
+ }),
865
+ new Lrclib({
866
+ timeoutMs: options.timeoutMs,
867
+ verbose: options.verbose
868
+ }),
869
+ new NetEase({
870
+ timeoutMs: options.timeoutMs,
871
+ verbose: options.verbose
872
+ }),
873
+ new Megalobiz({
874
+ timeoutMs: options.timeoutMs,
875
+ verbose: options.verbose
876
+ }),
877
+ new Genius({
878
+ timeoutMs: options.timeoutMs,
879
+ verbose: options.verbose
880
+ })
881
+ ];
882
+ const selectedProviders = selectProviders(providerInstances, options.providers, logger);
883
+ for (const provider of selectedProviders) {
884
+ logger.debug(`Looking for an LRC on ${provider.name}`);
885
+ try {
886
+ const result = await provider.getLrc(searchTerm);
887
+ if (result) {
888
+ lyrics.update(result);
889
+ }
890
+ } catch (err) {
891
+ logger.error(`An error occurred while searching for an LRC on ${provider.name}:`, err?.message ?? err);
892
+ if (options.lang) {
893
+ logger.error("Aborting, since `lang` is only supported by Musixmatch");
894
+ return null;
895
+ }
896
+ continue;
897
+ }
898
+ if (lyrics.isPreferred(targetType)) {
899
+ logger.info(`Lyrics found for "${searchTerm}" on ${provider.name}`);
900
+ break;
901
+ } else if (lyrics.isAcceptable(targetType)) {
902
+ logger.info(`Found plaintext lyrics on ${provider.name}, but continuing search for synced lyrics`);
903
+ } else {
904
+ logger.debug(`No suitable lyrics found on ${provider.name}, continuing search...`);
905
+ }
906
+ }
907
+ if (!lyrics.isAcceptable(targetType)) {
908
+ logger.info(`No suitable lyrics found for "${searchTerm}" :(`);
909
+ return null;
910
+ }
911
+ if (options.savePath) {
912
+ const formattedPath = options.savePath.replace(/{search_term}/g, searchTerm);
913
+ await lyrics.saveLrcFile(formattedPath, targetType);
914
+ }
915
+ return lyrics.toStr(targetType);
916
+ }
917
+ function selectProviders(allProviders, requestedNames, logger) {
918
+ if (!requestedNames || requestedNames.length === 0) {
919
+ return allProviders;
920
+ }
921
+ const lookup = new Set(requestedNames.map((name) => name.toLowerCase().trim()));
922
+ const filtered = allProviders.filter((p) => lookup.has(p.name.toLowerCase()));
923
+ if (filtered.length === 0) {
924
+ logger?.error(
925
+ `Providers [${requestedNames.join(", ")}] not found in the list of available providers.`
926
+ );
927
+ return [];
928
+ }
929
+ return filtered;
930
+ }
931
+ var synco = Object.assign(
932
+ function(searchTerm, options) {
933
+ return search(searchTerm, options);
934
+ },
935
+ {
936
+ search,
937
+ Lyrics,
938
+ TargetType,
939
+ LRCProvider,
940
+ Lrclib,
941
+ Megalobiz,
942
+ Musixmatch,
943
+ NetEase,
944
+ Genius,
945
+ Deezer,
946
+ Lyricsify
947
+ }
948
+ );
949
+
950
+ // src/cli.ts
951
+ async function runCli() {
952
+ const program = new import_commander.Command();
953
+ program.name("synco").description("Search for LRC format (synchronized lyrics) of a track").version("1.0.0", "-V, --version", "Output the version number").argument("<search_term>", 'The search term to find the track (e.g. "Song Name Artist")').option(
954
+ "-p, --providers <providers...>",
955
+ "Providers to include in the searching (e.g. lrclib megalobiz musixmatch netease genius)"
956
+ ).option("-l, --lang <lang>", "Language of the translation along with the lyrics").option("-o, --output <path>", "Path to save '.lrc' lyrics", "{search_term}.lrc").option("-v, --verbose", "Use this flag to show debug logs", false).option("--plain-only", "Only look for plain text (not synced) lyrics", false).option("--synced-only", "Only look for synced lyrics", false).option("--enhanced", "Returns word by word synced lyrics (if available)", false).option("--token <token>", "Musixmatch user token").action(async (searchTerm, opts) => {
957
+ try {
958
+ const lrc = await search(searchTerm, {
959
+ plainOnly: opts.plainOnly,
960
+ syncedOnly: opts.syncedOnly,
961
+ savePath: opts.output,
962
+ providers: opts.providers,
963
+ lang: opts.lang,
964
+ enhanced: opts.enhanced,
965
+ musixmatchToken: opts.token,
966
+ verbose: opts.verbose
967
+ });
968
+ if (lrc) {
969
+ console.log(lrc);
970
+ } else {
971
+ process.exitCode = 1;
972
+ }
973
+ } catch (err) {
974
+ console.error("Error:", err?.message ?? err);
975
+ process.exitCode = 1;
976
+ }
977
+ });
978
+ await program.parseAsync(process.argv);
979
+ }
980
+ runCli();
981
+ // Annotate the CommonJS export names for ESM import in node:
982
+ 0 && (module.exports = {
983
+ runCli
984
+ });