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 +53 -0
- package/README.md +22 -2
- package/dist/converter/index.d.ts +3 -3
- package/dist/converter/index.js +26 -15
- package/dist/converter/parse.js +18 -38
- package/dist/converter/youtube.js +4 -2
- package/dist/metadata-writer/index.d.ts +2 -0
- package/dist/metadata-writer/index.js +4 -1
- package/dist/metadata-writer/musixmatchLyrics.d.ts +16 -0
- package/dist/metadata-writer/musixmatchLyrics.js +64 -4
- package/package.json +1 -1
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
|
|
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
|
|
4
|
-
export
|
|
5
|
-
export
|
|
3
|
+
export declare const tidal: typeof import("./tidal");
|
|
4
|
+
export declare const spotify: typeof import("./spotify");
|
|
5
|
+
export declare const youtube: typeof import("./youtube");
|
package/dist/converter/index.js
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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'));
|
package/dist/converter/parse.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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