gerdur-core 2.18.0 → 2.19.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,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.19.0 - 2026-08-31
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **The Musixmatch lyrics fallback now latches off when it isn't working.**
|
|
8
|
+
For tracks Deezer has no lyrics for, the tagger scrapes Musixmatch — two page
|
|
9
|
+
loads each. Where Musixmatch blocks the request (it 403s from many networks and
|
|
10
|
+
most datacentres) every one of those still costs a round trip and still returns
|
|
11
|
+
nothing. Measured on a 14-track album: **2021 ms, 70% of total tagging time**,
|
|
12
|
+
for 3 KB of error pages.
|
|
13
|
+
|
|
14
|
+
After three consecutive *transport* failures the scraper stops being attempted
|
|
15
|
+
for the rest of the process. A track simply not being on Musixmatch does **not**
|
|
16
|
+
count toward that — the service is working fine in that case — so a run of
|
|
17
|
+
obscure tracks cannot disable a fallback that would otherwise succeed, and any
|
|
18
|
+
success resets the counter.
|
|
19
|
+
|
|
20
|
+
Measured, same album, default options: **2880 ms → 2641 ms on the first album
|
|
21
|
+
(the latch trips part-way through), then 653 / 729 ms — 77% faster in steady
|
|
22
|
+
state**, which is the case that matters for a library sync.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **`configureMusixmatch({maxFailures?, enabled?})`** and **`musixmatchStatus()`**
|
|
27
|
+
→ `{available, consecutiveFailures, maxFailures}`. Re-enabling clears the
|
|
28
|
+
count; nonsensical thresholds are rejected.
|
|
29
|
+
|
|
30
|
+
### Not done, deliberately
|
|
31
|
+
|
|
32
|
+
- Sending `Accept-Encoding` on the clients that don't set it. Measured the wire
|
|
33
|
+
first: a 14-track album transfers 176 KB, of which **111 KB is cover-art JPEG**
|
|
34
|
+
(incompressible) and 65 KB is `api.deezer.com` JSON that is **already gzipped**.
|
|
35
|
+
Musixmatch returns 3 KB total. The saving would have been ~0.
|
|
36
|
+
|
|
3
37
|
## 2.18.0 - 2026-08-31
|
|
4
38
|
|
|
5
39
|
Write operations — the first part of this package that changes an account.
|
package/README.md
CHANGED
|
@@ -589,7 +589,7 @@ model.contributors; // normalised producers / engineers / performers / …
|
|
|
589
589
|
| `album` / `lyrics` / `publicTrack` | — | pre-fetched payloads — pass once per album to skip refetching |
|
|
590
590
|
| `embedCover` / `embedArtistImage` | `true` | |
|
|
591
591
|
| `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
|
|
592
|
+
| `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
593
|
| `richCredits` | `true` | hydrate credits + BPM for album/playlist tracks that omit them |
|
|
594
594
|
| `deezerIds` / `includeRank` | `true` | write `DEEZER_*_ID` / popularity rank |
|
|
595
595
|
|
|
@@ -616,6 +616,22 @@ await pipeline(stream, createTagStream(model), createWriteStream('track.flac'));
|
|
|
616
616
|
`resolveTagModel(track, options?)` does exactly what `addTrackTags` does minus
|
|
617
617
|
the writing — same fetches, same coalescing, same `AddTrackTagsOptions`.
|
|
618
618
|
|
|
619
|
+
**The Musixmatch fallback looks after itself.** Where Musixmatch blocks you — it
|
|
620
|
+
403s from many networks and most datacentres — those scrapes cost a round trip
|
|
621
|
+
each and return nothing: 2021 ms on a 14-track album, 70% of tagging time. After
|
|
622
|
+
three consecutive *transport* failures it stops being attempted for the rest of
|
|
623
|
+
the process, taking that album to 653 ms. A track simply not being on Musixmatch
|
|
624
|
+
doesn't count toward the latch, so it can't disable itself where it actually
|
|
625
|
+
works, and any success resets it.
|
|
626
|
+
|
|
627
|
+
```ts
|
|
628
|
+
import {configureMusixmatch, musixmatchStatus} from 'gerdur-core';
|
|
629
|
+
|
|
630
|
+
musixmatchStatus(); // {available, consecutiveFailures, maxFailures}
|
|
631
|
+
configureMusixmatch({maxFailures: 5}); // more patient
|
|
632
|
+
configureMusixmatch({enabled: false}); // never scrape at all
|
|
633
|
+
```
|
|
634
|
+
|
|
619
635
|
Building blocks, if you want the model without writing tags:
|
|
620
636
|
|
|
621
637
|
- **`getRichAlbum(albId)`** → merged gw + public album metadata (`RichAlbum`).
|
|
@@ -860,7 +876,8 @@ import type {
|
|
|
860
876
|
`addFavoriteTracks` · `removeFavoriteTracks` · `addFavoriteAlbum` ·
|
|
861
877
|
`removeFavoriteAlbum` · `addFavoriteArtist` · `removeFavoriteArtist` ·
|
|
862
878
|
`followPlaylist` · `unfollowPlaylist` · `addFavoriteShow` · `createPlaylist` ·
|
|
863
|
-
`addTracksToPlaylist` · `removeTracksFromPlaylist`
|
|
879
|
+
`addTracksToPlaylist` · `removeTracksFromPlaylist` · `configureMusixmatch` ·
|
|
880
|
+
`musixmatchStatus`
|
|
864
881
|
</details>
|
|
865
882
|
|
|
866
883
|
<details>
|
|
@@ -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,47 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getLyricsMusixmatch = void 0;
|
|
3
|
+
exports.getLyricsMusixmatch = exports.musixmatchStatus = exports.configureMusixmatch = void 0;
|
|
4
4
|
const node_html_parser_1 = require("node-html-parser");
|
|
5
5
|
const http_1 = require("../lib/http");
|
|
6
6
|
const useragents_1 = require("./useragents");
|
|
7
7
|
const baseUrl = 'https://musixmatch.com';
|
|
8
|
+
/**
|
|
9
|
+
* Musixmatch is a *fallback* for tracks Deezer has no lyrics for — two scraped
|
|
10
|
+
* page loads each. Where Musixmatch blocks the request (it 403s from plenty of
|
|
11
|
+
* networks and datacentres) every one of those still costs a round trip and
|
|
12
|
+
* still returns nothing: measured at **2021 ms per 14-track album, 70% of all
|
|
13
|
+
* tagging time**, for 3 KB of error pages.
|
|
14
|
+
*
|
|
15
|
+
* So after a few consecutive *transport* failures the scraper latches off for
|
|
16
|
+
* the rest of the process. A track simply not being on Musixmatch does **not**
|
|
17
|
+
* count — that means the service is working fine — so a run of obscure tracks
|
|
18
|
+
* can't disable a fallback that would otherwise work. Any success resets it.
|
|
19
|
+
*/
|
|
20
|
+
let consecutiveTransportFailures = 0;
|
|
21
|
+
let latchedOff = false;
|
|
22
|
+
let maxFailures = 3;
|
|
23
|
+
/** Errors we raise ourselves for a legitimate miss, as opposed to the service failing. */
|
|
24
|
+
const MISS = /^No (song|lyrics) found!$/;
|
|
25
|
+
/** Tune or reset the Musixmatch fallback. */
|
|
26
|
+
const configureMusixmatch = (options = {}) => {
|
|
27
|
+
if (typeof options.maxFailures === 'number' && options.maxFailures > 0) {
|
|
28
|
+
maxFailures = Math.floor(options.maxFailures);
|
|
29
|
+
}
|
|
30
|
+
if (options.enabled !== undefined) {
|
|
31
|
+
latchedOff = !options.enabled;
|
|
32
|
+
if (options.enabled) {
|
|
33
|
+
consecutiveTransportFailures = 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
exports.configureMusixmatch = configureMusixmatch;
|
|
38
|
+
/** Whether the fallback is still being attempted, and how close it is to latching off. */
|
|
39
|
+
const musixmatchStatus = () => ({
|
|
40
|
+
available: !latchedOff,
|
|
41
|
+
consecutiveFailures: consecutiveTransportFailures,
|
|
42
|
+
maxFailures,
|
|
43
|
+
});
|
|
44
|
+
exports.musixmatchStatus = musixmatchStatus;
|
|
8
45
|
const getUrlMusixmatch = async (query) => {
|
|
9
46
|
var _a;
|
|
10
47
|
const data = await (0, http_1.getText)(`${baseUrl}/search/${encodeURI(query)}/tracks`, {
|
|
@@ -20,7 +57,7 @@ const getUrlMusixmatch = async (query) => {
|
|
|
20
57
|
}
|
|
21
58
|
throw new Error('No song found!');
|
|
22
59
|
};
|
|
23
|
-
const
|
|
60
|
+
const scrape = async (query) => {
|
|
24
61
|
const url = await getUrlMusixmatch(query);
|
|
25
62
|
const data = await (0, http_1.getText)(url, {
|
|
26
63
|
headers: {
|
|
@@ -36,4 +73,25 @@ const getLyricsMusixmatch = async (query) => {
|
|
|
36
73
|
lyrics = lyrics.replace('"body":"', '').replace('","language"', '');
|
|
37
74
|
return lyrics.split('\\n').join('\n');
|
|
38
75
|
};
|
|
76
|
+
const getLyricsMusixmatch = async (query) => {
|
|
77
|
+
var _a;
|
|
78
|
+
if (latchedOff) {
|
|
79
|
+
throw new Error('Musixmatch fallback is unavailable from this network (latched off)');
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const lyrics = await scrape(query);
|
|
83
|
+
consecutiveTransportFailures = 0; // it works — forget any earlier trouble
|
|
84
|
+
return lyrics;
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
// a track that simply isn't there says nothing about the service
|
|
88
|
+
if (!MISS.test((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : '')) {
|
|
89
|
+
consecutiveTransportFailures += 1;
|
|
90
|
+
if (consecutiveTransportFailures >= maxFailures) {
|
|
91
|
+
latchedOff = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
39
97
|
exports.getLyricsMusixmatch = getLyricsMusixmatch;
|
package/package.json
CHANGED