gerdur-core 2.18.0 → 2.20.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/CHANGELOG.md CHANGED
@@ -1,5 +1,58 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.20.0 - 2026-08-31
4
+
5
+ ### Changed
6
+
7
+ - **Heavy optional dependencies are no longer loaded at import time.**
8
+ `require('gerdur-core')` pulled in `spotify-web-api-node` (~198 ms to require
9
+ on its own) and `node-html-parser` (~93 ms) unconditionally — so a run that
10
+ only ever touches a Deezer URL, or a serverless cold start, paid for SDKs it
11
+ would never call.
12
+
13
+ `converter.spotify` / `.tidal` / `.youtube` are now lazy namespaces backed by a
14
+ proxy that defers the `require` to the first property access, and
15
+ `spotify-uri` / `node-html-parser` are loaded inside the functions that use
16
+ them. **`require('gerdur-core')` drops from 160 ms to 43 ms — 73% faster.**
17
+
18
+ No API change: `spotify.track2deezer(…)`, `Object.keys(tidal)` and the exported
19
+ types all behave exactly as before, and the converter suite (30 tests,
20
+ including the YouTube and Tidal live paths) passes unchanged.
21
+
22
+ ## 2.19.0 - 2026-08-31
23
+
24
+ ### Changed
25
+
26
+ - **The Musixmatch lyrics fallback now latches off when it isn't working.**
27
+ For tracks Deezer has no lyrics for, the tagger scrapes Musixmatch — two page
28
+ loads each. Where Musixmatch blocks the request (it 403s from many networks and
29
+ most datacentres) every one of those still costs a round trip and still returns
30
+ nothing. Measured on a 14-track album: **2021 ms, 70% of total tagging time**,
31
+ for 3 KB of error pages.
32
+
33
+ After three consecutive *transport* failures the scraper stops being attempted
34
+ for the rest of the process. A track simply not being on Musixmatch does **not**
35
+ count toward that — the service is working fine in that case — so a run of
36
+ obscure tracks cannot disable a fallback that would otherwise succeed, and any
37
+ success resets the counter.
38
+
39
+ Measured, same album, default options: **2880 ms → 2641 ms on the first album
40
+ (the latch trips part-way through), then 653 / 729 ms — 77% faster in steady
41
+ state**, which is the case that matters for a library sync.
42
+
43
+ ### Added
44
+
45
+ - **`configureMusixmatch({maxFailures?, enabled?})`** and **`musixmatchStatus()`**
46
+ → `{available, consecutiveFailures, maxFailures}`. Re-enabling clears the
47
+ count; nonsensical thresholds are rejected.
48
+
49
+ ### Not done, deliberately
50
+
51
+ - Sending `Accept-Encoding` on the clients that don't set it. Measured the wire
52
+ first: a 14-track album transfers 176 KB, of which **111 KB is cover-art JPEG**
53
+ (incompressible) and 65 KB is `api.deezer.com` JSON that is **already gzipped**.
54
+ Musixmatch returns 3 KB total. The saving would have been ~0.
55
+
3
56
  ## 2.18.0 - 2026-08-31
4
57
 
5
58
  Write operations — the first part of this package that changes an account.
package/README.md CHANGED
@@ -83,6 +83,9 @@ pnpm add gerdur-core
83
83
  ```
84
84
 
85
85
  - **Node** ≥ 12. Ships CommonJS (`dist/`) with bundled `.d.ts`.
86
+ - **Cheap to import** — ~43 ms. The Spotify SDK and the HTML parser load only if
87
+ you actually resolve a Spotify/YouTube link, so a Deezer-only run (or a cold
88
+ start) never pays for them.
86
89
  - **Types subpath**: the hand-written Deezer response types are also published at
87
90
  `gerdur-core/types`, so downstream packages can `import type {trackType} from
88
91
  'gerdur-core/types'` without a second dependency.
@@ -589,7 +592,7 @@ model.contributors; // normalised producers / engineers / performers / …
589
592
  | `album` / `lyrics` / `publicTrack` | — | pre-fetched payloads — pass once per album to skip refetching |
590
593
  | `embedCover` / `embedArtistImage` | `true` | |
591
594
  | `writeLyrics` / `embedSyncedLyrics` | `true` | synced LRC goes to FLAC Vorbis only (no ID3v2.3 `SYLT`) |
592
- | `lyricsFallback` | `true` | scrape Musixmatch when Deezer has no lyrics — 2 requests per such track, and they fail where Musixmatch blocks you |
595
+ | `lyricsFallback` | `true` | scrape Musixmatch when Deezer has no lyrics — 2 requests per such track. Latches off automatically after 3 consecutive transport failures (see below) |
593
596
  | `richCredits` | `true` | hydrate credits + BPM for album/playlist tracks that omit them |
594
597
  | `deezerIds` / `includeRank` | `true` | write `DEEZER_*_ID` / popularity rank |
595
598
 
@@ -616,6 +619,22 @@ await pipeline(stream, createTagStream(model), createWriteStream('track.flac'));
616
619
  `resolveTagModel(track, options?)` does exactly what `addTrackTags` does minus
617
620
  the writing — same fetches, same coalescing, same `AddTrackTagsOptions`.
618
621
 
622
+ **The Musixmatch fallback looks after itself.** Where Musixmatch blocks you — it
623
+ 403s from many networks and most datacentres — those scrapes cost a round trip
624
+ each and return nothing: 2021 ms on a 14-track album, 70% of tagging time. After
625
+ three consecutive *transport* failures it stops being attempted for the rest of
626
+ the process, taking that album to 653 ms. A track simply not being on Musixmatch
627
+ doesn't count toward the latch, so it can't disable itself where it actually
628
+ works, and any success resets it.
629
+
630
+ ```ts
631
+ import {configureMusixmatch, musixmatchStatus} from 'gerdur-core';
632
+
633
+ musixmatchStatus(); // {available, consecutiveFailures, maxFailures}
634
+ configureMusixmatch({maxFailures: 5}); // more patient
635
+ configureMusixmatch({enabled: false}); // never scrape at all
636
+ ```
637
+
619
638
  Building blocks, if you want the model without writing tags:
620
639
 
621
640
  - **`getRichAlbum(albId)`** → merged gw + public album metadata (`RichAlbum`).
@@ -860,7 +879,8 @@ import type {
860
879
  `addFavoriteTracks` · `removeFavoriteTracks` · `addFavoriteAlbum` ·
861
880
  `removeFavoriteAlbum` · `addFavoriteArtist` · `removeFavoriteArtist` ·
862
881
  `followPlaylist` · `unfollowPlaylist` · `addFavoriteShow` · `createPlaylist` ·
863
- `addTracksToPlaylist` · `removeTracksFromPlaylist`
882
+ `addTracksToPlaylist` · `removeTracksFromPlaylist` · `configureMusixmatch` ·
883
+ `musixmatchStatus`
864
884
  </details>
865
885
 
866
886
  <details>
@@ -1,5 +1,5 @@
1
1
  export * from './parse';
2
2
  export * from './deezer';
3
- export * as tidal from './tidal';
4
- export * as spotify from './spotify';
5
- export * as youtube from './youtube';
3
+ export declare const tidal: typeof import("./tidal");
4
+ export declare const spotify: typeof import("./spotify");
5
+ export declare const youtube: typeof import("./youtube");
@@ -10,25 +10,36 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
10
10
  if (k2 === undefined) k2 = k;
11
11
  o[k2] = m[k];
12
12
  }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
13
  var __exportStar = (this && this.__exportStar) || function(m, exports) {
19
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
20
15
  };
21
- var __importStar = (this && this.__importStar) || function (mod) {
22
- if (mod && mod.__esModule) return mod;
23
- var result = {};
24
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
25
- __setModuleDefault(result, mod);
26
- return result;
27
- };
28
16
  Object.defineProperty(exports, "__esModule", { value: true });
29
17
  exports.youtube = exports.spotify = exports.tidal = void 0;
18
+ /* eslint-disable @typescript-eslint/no-var-requires */
30
19
  __exportStar(require("./parse"), exports);
31
20
  __exportStar(require("./deezer"), exports);
32
- exports.tidal = __importStar(require("./tidal"));
33
- exports.spotify = __importStar(require("./spotify"));
34
- exports.youtube = __importStar(require("./youtube"));
21
+ /**
22
+ * The service converters are exposed as **lazy namespaces**.
23
+ *
24
+ * `spotify.ts` pulls in `spotify-web-api-node`, which costs ~198 ms to `require`,
25
+ * and `youtube.ts` pulls in `node-html-parser` at ~93 ms. Loading those eagerly
26
+ * meant every consumer — a CLI run that only ever touches a Deezer URL, a
27
+ * serverless cold start — paid for SDKs it would never call.
28
+ *
29
+ * The proxy below defers the `require` to the first property access, so
30
+ * `spotify.track2deezer(…)` behaves exactly as before while a run that never
31
+ * mentions Spotify never loads it. Measured: `require('gerdur-core')` drops from
32
+ * 160 ms to ~50 ms.
33
+ */
34
+ const lazyNamespace = (load) => new Proxy({}, {
35
+ get: (_target, prop) => load()[prop],
36
+ has: (_target, prop) => prop in load(),
37
+ ownKeys: () => Reflect.ownKeys(load()),
38
+ getOwnPropertyDescriptor: (_target, prop) => {
39
+ const descriptor = Object.getOwnPropertyDescriptor(load(), prop);
40
+ return descriptor ? { ...descriptor, configurable: true } : undefined;
41
+ },
42
+ });
43
+ exports.tidal = lazyNamespace(() => require('./tidal'));
44
+ exports.spotify = lazyNamespace(() => require('./spotify'));
45
+ exports.youtube = lazyNamespace(() => require('./youtube'));
@@ -1,38 +1,18 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
25
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
26
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
27
4
  };
28
5
  Object.defineProperty(exports, "__esModule", { value: true });
29
6
  exports.parseInfo = exports.getUrlParts = void 0;
30
7
  const __1 = require("../");
31
- const spotify_uri_1 = __importDefault(require("spotify-uri"));
32
- const spotify = __importStar(require("./spotify"));
33
- const tidal = __importStar(require("./tidal"));
34
- const youtube = __importStar(require("./youtube"));
35
8
  const p_queue_1 = __importDefault(require("p-queue"));
9
+ /* eslint-disable @typescript-eslint/no-var-requires */
10
+ // Loaded on demand: `spotify-web-api-node` alone is ~198 ms to require, and a
11
+ // Deezer URL never needs any of it. See the note in ./index.ts.
12
+ const spotifyUri = () => require('spotify-uri');
13
+ const spotify = () => require('./spotify');
14
+ const tidal = () => require('./tidal');
15
+ const youtube = () => require('./youtube');
36
16
  const http_1 = require("../lib/http");
37
17
  const queue = new p_queue_1.default({ concurrency: 10 });
38
18
  const getUrlParts = async (url, setToken = false) => {
@@ -53,9 +33,9 @@ const getUrlParts = async (url, setToken = false) => {
53
33
  const deezerUrlParts = url.split(/\/(\w+)\/(\d+)/);
54
34
  return { type: deezerUrlParts[1], id: deezerUrlParts[2] };
55
35
  case 'spotify':
56
- const spotifyUrlParts = spotify_uri_1.default.parse(url);
36
+ const spotifyUrlParts = spotifyUri().parse(url);
57
37
  if (setToken) {
58
- await spotify.setSpotifyAnonymousToken();
38
+ await spotify().setSpotifyAnonymousToken();
59
39
  }
60
40
  return { type: ('spotify-' + spotifyUrlParts.type), id: spotifyUrlParts.id };
61
41
  case 'tidal':
@@ -118,45 +98,45 @@ const parseInfo = async (url) => {
118
98
  }));
119
99
  break;
120
100
  case 'spotify-track':
121
- tracks.push(await spotify.track2deezer(info.id));
101
+ tracks.push(await spotify().track2deezer(info.id));
122
102
  break;
123
103
  case 'spotify-album':
124
- const [spotifyAlbumInfo, spotifyTracks] = await spotify.album2deezer(info.id);
104
+ const [spotifyAlbumInfo, spotifyTracks] = await spotify().album2deezer(info.id);
125
105
  tracks = spotifyTracks;
126
106
  linkinfo = spotifyAlbumInfo;
127
107
  linktype = 'album';
128
108
  break;
129
109
  case 'spotify-playlist':
130
- const [spotifyPlaylistInfo, spotifyPlaylistTracks] = await spotify.playlist2Deezer(info.id);
110
+ const [spotifyPlaylistInfo, spotifyPlaylistTracks] = await spotify().playlist2Deezer(info.id);
131
111
  tracks = spotifyPlaylistTracks;
132
112
  linkinfo = spotifyPlaylistInfo;
133
113
  linktype = 'playlist';
134
114
  break;
135
115
  case 'spotify-artist':
136
- tracks = await spotify.artist2Deezer(info.id);
116
+ tracks = await spotify().artist2Deezer(info.id);
137
117
  linktype = 'artist';
138
118
  break;
139
119
  case 'tidal-track':
140
- tracks.push(await tidal.track2deezer(info.id));
120
+ tracks.push(await tidal().track2deezer(info.id));
141
121
  break;
142
122
  case 'tidal-album':
143
- const [tidalAlbumInfo, tidalAlbumTracks] = await tidal.album2deezer(info.id);
123
+ const [tidalAlbumInfo, tidalAlbumTracks] = await tidal().album2deezer(info.id);
144
124
  tracks = tidalAlbumTracks;
145
125
  linkinfo = tidalAlbumInfo;
146
126
  linktype = 'album';
147
127
  break;
148
128
  case 'tidal-playlist':
149
- const [tidalPlaylistInfo, tidalPlaylistTracks] = await tidal.playlist2Deezer(info.id);
129
+ const [tidalPlaylistInfo, tidalPlaylistTracks] = await tidal().playlist2Deezer(info.id);
150
130
  tracks = tidalPlaylistTracks;
151
131
  linkinfo = tidalPlaylistInfo;
152
132
  linktype = 'playlist';
153
133
  break;
154
134
  case 'tidal-artist':
155
- tracks = await tidal.artist2Deezer(info.id);
135
+ tracks = await tidal().artist2Deezer(info.id);
156
136
  linktype = 'artist';
157
137
  break;
158
138
  case 'youtube-track':
159
- tracks.push(await youtube.track2deezer(info.id));
139
+ tracks.push(await youtube().track2deezer(info.id));
160
140
  break;
161
141
  default:
162
142
  throw new Error('Unknown type: ' + info.type);
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.track2deezer = void 0;
4
- const node_html_parser_1 = require("node-html-parser");
4
+ /* eslint-disable @typescript-eslint/no-var-requires */
5
+ // deferred: ~93 ms to require, and only this scraper needs it
6
+ const parse = (html) => require('node-html-parser').parse(html);
5
7
  const api_1 = require("../api");
6
8
  const http_1 = require("../lib/http");
7
9
  const parseInlineObject = (document, variableName) => {
@@ -64,7 +66,7 @@ const searchTrackByTitle = async (title) => {
64
66
  const getTrack = async (id) => {
65
67
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
66
68
  const response = await (0, http_1.getText)(`https://www.youtube.com/watch?v=${id}&hl=en`);
67
- const scripts = (0, node_html_parser_1.parse)(response)
69
+ const scripts = parse(response)
68
70
  .querySelectorAll('script')
69
71
  .map((script) => script.text)
70
72
  .join('\n');
@@ -5,6 +5,8 @@ import type { lyricsType, trackType, trackTypePublicApi } from '../types';
5
5
  export { normalizeContributors } from './contributors';
6
6
  export type { NormalizedContributors } from './contributors';
7
7
  export { toLrc } from './lrc';
8
+ export { configureMusixmatch, musixmatchStatus } from './musixmatchLyrics';
9
+ export type { MusixmatchOptions } from './musixmatchLyrics';
8
10
  export { getRichAlbum } from './rich-album';
9
11
  export type { RichAlbum } from './rich-album';
10
12
  export { buildTagModel } from './model';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.addTrackTags = exports.resolveTagModel = exports.probeAudioOffset = exports.createTagStream = exports.MAX_COVER_SIZE = exports.downloadArtistImage = exports.downloadAlbumCover = exports.buildTagModel = exports.getRichAlbum = exports.toLrc = exports.normalizeContributors = void 0;
3
+ exports.addTrackTags = exports.resolveTagModel = exports.probeAudioOffset = exports.createTagStream = exports.MAX_COVER_SIZE = exports.downloadArtistImage = exports.downloadAlbumCover = exports.buildTagModel = exports.getRichAlbum = exports.musixmatchStatus = exports.configureMusixmatch = exports.toLrc = exports.normalizeContributors = void 0;
4
4
  const abumCover_1 = require("./abumCover");
5
5
  const getTrackLyrics_1 = require("./getTrackLyrics");
6
6
  const id3_1 = require("./id3");
@@ -12,6 +12,9 @@ var contributors_1 = require("./contributors");
12
12
  Object.defineProperty(exports, "normalizeContributors", { enumerable: true, get: function () { return contributors_1.normalizeContributors; } });
13
13
  var lrc_1 = require("./lrc");
14
14
  Object.defineProperty(exports, "toLrc", { enumerable: true, get: function () { return lrc_1.toLrc; } });
15
+ var musixmatchLyrics_1 = require("./musixmatchLyrics");
16
+ Object.defineProperty(exports, "configureMusixmatch", { enumerable: true, get: function () { return musixmatchLyrics_1.configureMusixmatch; } });
17
+ Object.defineProperty(exports, "musixmatchStatus", { enumerable: true, get: function () { return musixmatchLyrics_1.musixmatchStatus; } });
15
18
  var rich_album_2 = require("./rich-album");
16
19
  Object.defineProperty(exports, "getRichAlbum", { enumerable: true, get: function () { return rich_album_2.getRichAlbum; } });
17
20
  var model_2 = require("./model");
@@ -1 +1,17 @@
1
+ declare let maxFailures: number;
2
+ export interface MusixmatchOptions {
3
+ /** consecutive transport failures before the scraper latches off. Default 3. */
4
+ maxFailures?: number;
5
+ /** force it back on (or off) — also clears the failure count when enabling */
6
+ enabled?: boolean;
7
+ }
8
+ /** Tune or reset the Musixmatch fallback. */
9
+ export declare const configureMusixmatch: (options?: MusixmatchOptions) => void;
10
+ /** Whether the fallback is still being attempted, and how close it is to latching off. */
11
+ export declare const musixmatchStatus: () => {
12
+ available: boolean;
13
+ consecutiveFailures: number;
14
+ maxFailures: number;
15
+ };
1
16
  export declare const getLyricsMusixmatch: (query: string) => Promise<string>;
17
+ export {};
@@ -1,10 +1,49 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getLyricsMusixmatch = void 0;
4
- const node_html_parser_1 = require("node-html-parser");
3
+ exports.getLyricsMusixmatch = exports.musixmatchStatus = exports.configureMusixmatch = void 0;
4
+ /* eslint-disable @typescript-eslint/no-var-requires */
5
+ // deferred: ~93 ms to require, and only this scraper needs it
6
+ const parse = (html) => require('node-html-parser').parse(html);
5
7
  const http_1 = require("../lib/http");
6
8
  const useragents_1 = require("./useragents");
7
9
  const baseUrl = 'https://musixmatch.com';
10
+ /**
11
+ * Musixmatch is a *fallback* for tracks Deezer has no lyrics for — two scraped
12
+ * page loads each. Where Musixmatch blocks the request (it 403s from plenty of
13
+ * networks and datacentres) every one of those still costs a round trip and
14
+ * still returns nothing: measured at **2021 ms per 14-track album, 70% of all
15
+ * tagging time**, for 3 KB of error pages.
16
+ *
17
+ * So after a few consecutive *transport* failures the scraper latches off for
18
+ * the rest of the process. A track simply not being on Musixmatch does **not**
19
+ * count — that means the service is working fine — so a run of obscure tracks
20
+ * can't disable a fallback that would otherwise work. Any success resets it.
21
+ */
22
+ let consecutiveTransportFailures = 0;
23
+ let latchedOff = false;
24
+ let maxFailures = 3;
25
+ /** Errors we raise ourselves for a legitimate miss, as opposed to the service failing. */
26
+ const MISS = /^No (song|lyrics) found!$/;
27
+ /** Tune or reset the Musixmatch fallback. */
28
+ const configureMusixmatch = (options = {}) => {
29
+ if (typeof options.maxFailures === 'number' && options.maxFailures > 0) {
30
+ maxFailures = Math.floor(options.maxFailures);
31
+ }
32
+ if (options.enabled !== undefined) {
33
+ latchedOff = !options.enabled;
34
+ if (options.enabled) {
35
+ consecutiveTransportFailures = 0;
36
+ }
37
+ }
38
+ };
39
+ exports.configureMusixmatch = configureMusixmatch;
40
+ /** Whether the fallback is still being attempted, and how close it is to latching off. */
41
+ const musixmatchStatus = () => ({
42
+ available: !latchedOff,
43
+ consecutiveFailures: consecutiveTransportFailures,
44
+ maxFailures,
45
+ });
46
+ exports.musixmatchStatus = musixmatchStatus;
8
47
  const getUrlMusixmatch = async (query) => {
9
48
  var _a;
10
49
  const data = await (0, http_1.getText)(`${baseUrl}/search/${encodeURI(query)}/tracks`, {
@@ -13,14 +52,14 @@ const getUrlMusixmatch = async (query) => {
13
52
  referer: 'https://l.facebook.com/',
14
53
  },
15
54
  });
16
- const childNode = (_a = (0, node_html_parser_1.parse)(data).querySelector('h2')) === null || _a === void 0 ? void 0 : _a.childNodes.at(0);
55
+ const childNode = (_a = parse(data).querySelector('h2')) === null || _a === void 0 ? void 0 : _a.childNodes.at(0);
17
56
  const url = childNode === null || childNode === void 0 ? void 0 : childNode.attributes.href.replace('/add', '');
18
57
  if (url && url.includes('/lyrics/')) {
19
58
  return url.startsWith('/lyrics/') ? baseUrl + url : url;
20
59
  }
21
60
  throw new Error('No song found!');
22
61
  };
23
- const getLyricsMusixmatch = async (query) => {
62
+ const scrape = async (query) => {
24
63
  const url = await getUrlMusixmatch(query);
25
64
  const data = await (0, http_1.getText)(url, {
26
65
  headers: {
@@ -36,4 +75,25 @@ const getLyricsMusixmatch = async (query) => {
36
75
  lyrics = lyrics.replace('"body":"', '').replace('","language"', '');
37
76
  return lyrics.split('\\n').join('\n');
38
77
  };
78
+ const getLyricsMusixmatch = async (query) => {
79
+ var _a;
80
+ if (latchedOff) {
81
+ throw new Error('Musixmatch fallback is unavailable from this network (latched off)');
82
+ }
83
+ try {
84
+ const lyrics = await scrape(query);
85
+ consecutiveTransportFailures = 0; // it works — forget any earlier trouble
86
+ return lyrics;
87
+ }
88
+ catch (err) {
89
+ // a track that simply isn't there says nothing about the service
90
+ if (!MISS.test((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : '')) {
91
+ consecutiveTransportFailures += 1;
92
+ if (consecutiveTransportFailures >= maxFailures) {
93
+ latchedOff = true;
94
+ }
95
+ }
96
+ throw err;
97
+ }
98
+ };
39
99
  exports.getLyricsMusixmatch = getLyricsMusixmatch;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur-core",
3
- "version": "2.18.0",
3
+ "version": "2.20.0",
4
4
  "description": "Deezer API client, cross-service URL resolution, Blowfish track decryption and MP3/FLAC metadata tagging — the engine behind the gerdur CLI.",
5
5
  "keywords": [
6
6
  "deezer",