gerdur-core 2.17.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 +74 -0
- package/README.md +61 -2
- package/dist/api/favorites.d.ts +59 -0
- package/dist/api/favorites.js +99 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- 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 +60 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
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
|
+
|
|
37
|
+
## 2.18.0 - 2026-08-31
|
|
38
|
+
|
|
39
|
+
Write operations — the first part of this package that changes an account.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **`src/api/favorites.ts`** — mutating library operations, in their own module
|
|
44
|
+
and wired into no download path, so they only run when called deliberately:
|
|
45
|
+
- `addFavoriteTracks(ids)` / `removeFavoriteTracks(ids)` (`song.addFavorites` /
|
|
46
|
+
`song.removeFavorites` — note the plural)
|
|
47
|
+
- `addFavoriteAlbum` / `removeFavoriteAlbum`, `addFavoriteArtist` /
|
|
48
|
+
`removeFavoriteArtist`
|
|
49
|
+
- `followPlaylist` / `unfollowPlaylist` (the gateway wants
|
|
50
|
+
`parent_playlist_id` to follow but `playlist_id` to unfollow)
|
|
51
|
+
- `addFavoriteShow`, `createPlaylist`, `addTracksToPlaylist`,
|
|
52
|
+
`removeTracksFromPlaylist`
|
|
53
|
+
|
|
54
|
+
The surface was mapped by calling each candidate with **incomplete
|
|
55
|
+
parameters** — an existing method answers `MISSING_PARAMETER_*`, an absent one
|
|
56
|
+
`GATEWAY_ERROR` — so method names and their parameter names were established
|
|
57
|
+
without creating or changing anything.
|
|
58
|
+
|
|
59
|
+
### Verification status
|
|
60
|
+
|
|
61
|
+
- Method existence and parameter names: **confirmed by probe**.
|
|
62
|
+
- End-to-end execution: **not run.** `__tests__/favorites.ts` contains
|
|
63
|
+
snapshot → mutate → assert → revert → assert-restored round trips, gated
|
|
64
|
+
behind `GERDUR_ALLOW_WRITE_TESTS=1` so they never fire by accident.
|
|
65
|
+
|
|
66
|
+
### Known gaps
|
|
67
|
+
|
|
68
|
+
- **Deezer's gateway has no playlist delete.** `playlist.delete`, `.remove`,
|
|
69
|
+
`.destroy`, `.deletePlaylist` and eight other spellings all answer
|
|
70
|
+
`GATEWAY_ERROR`, so `createPlaylist` is one-way — remove what it makes from a
|
|
71
|
+
Deezer client. Everything else here has a confirmed inverse.
|
|
72
|
+
- No `show.deleteFavorite` was found, so `addFavoriteShow` is one-way too.
|
|
73
|
+
- **`log.listen` (scrobbling) is not implemented.** It never answered the probe,
|
|
74
|
+
so its shape is unknown, and it writes to listening history that cannot be
|
|
75
|
+
undone — not something to ship on a guess.
|
|
76
|
+
|
|
3
77
|
## 2.17.0 - 2026-08-31
|
|
4
78
|
|
|
5
79
|
The account's own library, over the authenticated gateway.
|
package/README.md
CHANGED
|
@@ -47,6 +47,7 @@ the CLI and the file-writing layer on top.
|
|
|
47
47
|
- [Browse and discover](#browse-and-discover)
|
|
48
48
|
- [Flow, radios and a user's library](#flow-radios-and-a-users-library)
|
|
49
49
|
- [Your own library](#your-own-library)
|
|
50
|
+
- [Changing the account (write operations)](#changing-the-account-write-operations)
|
|
50
51
|
- [Podcasts](#podcasts)
|
|
51
52
|
- [Preview clips](#preview-clips)
|
|
52
53
|
- [Resolve a download URL](#resolve-a-download-url)
|
|
@@ -409,6 +410,43 @@ const urls = await resolveDownloadUrls(mix, [9, 3, 1]); // straight to download
|
|
|
409
410
|
`userId` defaults to the logged-in account. These are account-scoped, so they
|
|
410
411
|
never enter the shared cross-session cache.
|
|
411
412
|
|
|
413
|
+
### Changing the account (write operations)
|
|
414
|
+
|
|
415
|
+
Everything above reads. These change the logged-in account's library for real —
|
|
416
|
+
they are in their own module, wired into no download path, and only run when you
|
|
417
|
+
call them.
|
|
418
|
+
|
|
419
|
+
| Function | Inverse |
|
|
420
|
+
| :--- | :--- |
|
|
421
|
+
| `addFavoriteTracks(ids)` | `removeFavoriteTracks(ids)` |
|
|
422
|
+
| `addFavoriteAlbum(id)` | `removeFavoriteAlbum(id)` |
|
|
423
|
+
| `addFavoriteArtist(id)` | `removeFavoriteArtist(id)` |
|
|
424
|
+
| `followPlaylist(id)` | `unfollowPlaylist(id)` |
|
|
425
|
+
| `addFavoriteShow(id)` | — none found |
|
|
426
|
+
| `createPlaylist(title, opts?)` | **— none exists, see below** |
|
|
427
|
+
| `addTracksToPlaylist(id, sngIds)` | `removeTracksFromPlaylist(id, sngIds)` |
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
import {addFavoriteTracks, removeFavoriteTracks} from 'gerdur-core';
|
|
431
|
+
|
|
432
|
+
await addFavoriteTracks(['3135556']); // love it
|
|
433
|
+
await removeFavoriteTracks(['3135556']); // and back
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
> **`createPlaylist` is a one-way door.** Deezer's gateway exposes no delete —
|
|
437
|
+
> a dozen spellings of `playlist.delete` all answer `GATEWAY_ERROR` — so a
|
|
438
|
+
> playlist made here has to be removed from a Deezer client. It is also the one
|
|
439
|
+
> function in this module that has not been exercised against a live account,
|
|
440
|
+
> for exactly that reason.
|
|
441
|
+
>
|
|
442
|
+
> Method names and parameters were established by probing with incomplete
|
|
443
|
+
> arguments (an existing method answers `MISSING_PARAMETER_*`), so the surface is
|
|
444
|
+
> real; the round trips live in `__tests__/favorites.ts` behind
|
|
445
|
+
> `GERDUR_ALLOW_WRITE_TESTS=1`.
|
|
446
|
+
|
|
447
|
+
Scrobbling (`log.listen`) is deliberately absent: it never answered the probe and
|
|
448
|
+
it writes history that cannot be undone.
|
|
449
|
+
|
|
412
450
|
### Podcasts
|
|
413
451
|
|
|
414
452
|
```ts
|
|
@@ -551,7 +589,7 @@ model.contributors; // normalised producers / engineers / performers / …
|
|
|
551
589
|
| `album` / `lyrics` / `publicTrack` | — | pre-fetched payloads — pass once per album to skip refetching |
|
|
552
590
|
| `embedCover` / `embedArtistImage` | `true` | |
|
|
553
591
|
| `writeLyrics` / `embedSyncedLyrics` | `true` | synced LRC goes to FLAC Vorbis only (no ID3v2.3 `SYLT`) |
|
|
554
|
-
| `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) |
|
|
555
593
|
| `richCredits` | `true` | hydrate credits + BPM for album/playlist tracks that omit them |
|
|
556
594
|
| `deezerIds` / `includeRank` | `true` | write `DEEZER_*_ID` / popularity rank |
|
|
557
595
|
|
|
@@ -578,6 +616,22 @@ await pipeline(stream, createTagStream(model), createWriteStream('track.flac'));
|
|
|
578
616
|
`resolveTagModel(track, options?)` does exactly what `addTrackTags` does minus
|
|
579
617
|
the writing — same fetches, same coalescing, same `AddTrackTagsOptions`.
|
|
580
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
|
+
|
|
581
635
|
Building blocks, if you want the model without writing tags:
|
|
582
636
|
|
|
583
637
|
- **`getRichAlbum(albId)`** → merged gw + public album metadata (`RichAlbum`).
|
|
@@ -818,7 +872,12 @@ import type {
|
|
|
818
872
|
`getUserChartTracks` · `getRadios` · `getRadioTracks` · `getRadioGenres` ·
|
|
819
873
|
`getMyPlaylists` · `getMyFavoriteTracks` · `getMyFavoriteTrackIds` ·
|
|
820
874
|
`getMyFavoriteAlbums` · `getMyFavoriteArtists` · `getMyFavoritePlaylists` ·
|
|
821
|
-
`getMyFavoriteRadios` · `getMyFavoriteShows` · `getTrackMix`
|
|
875
|
+
`getMyFavoriteRadios` · `getMyFavoriteShows` · `getTrackMix` ·
|
|
876
|
+
`addFavoriteTracks` · `removeFavoriteTracks` · `addFavoriteAlbum` ·
|
|
877
|
+
`removeFavoriteAlbum` · `addFavoriteArtist` · `removeFavoriteArtist` ·
|
|
878
|
+
`followPlaylist` · `unfollowPlaylist` · `addFavoriteShow` · `createPlaylist` ·
|
|
879
|
+
`addTracksToPlaylist` · `removeTracksFromPlaylist` · `configureMusixmatch` ·
|
|
880
|
+
`musixmatchStatus`
|
|
822
881
|
</details>
|
|
823
882
|
|
|
824
883
|
<details>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** What the gateway returns for a mutation — usually `true`, or the new id. */
|
|
2
|
+
export type WriteResult = boolean | string | number | Record<string, any>;
|
|
3
|
+
/**
|
|
4
|
+
* Love one or more tracks (`song.addFavorites`). Reversible with
|
|
5
|
+
* {@link removeFavoriteTracks}.
|
|
6
|
+
*
|
|
7
|
+
* @param sngIds `SNG_ID`s
|
|
8
|
+
*/
|
|
9
|
+
export declare const addFavoriteTracks: (sngIds: (string | number)[]) => Promise<WriteResult>;
|
|
10
|
+
/** Un-love one or more tracks (`song.removeFavorites`). */
|
|
11
|
+
export declare const removeFavoriteTracks: (sngIds: (string | number)[]) => Promise<WriteResult>;
|
|
12
|
+
/** Add an album to favourites (`album.addFavorite`). */
|
|
13
|
+
export declare const addFavoriteAlbum: (albId: string | number) => Promise<WriteResult>;
|
|
14
|
+
/** Remove an album from favourites (`album.deleteFavorite`). */
|
|
15
|
+
export declare const removeFavoriteAlbum: (albId: string | number) => Promise<WriteResult>;
|
|
16
|
+
/** Follow an artist (`artist.addFavorite`). */
|
|
17
|
+
export declare const addFavoriteArtist: (artId: string | number) => Promise<WriteResult>;
|
|
18
|
+
/** Unfollow an artist (`artist.deleteFavorite`). */
|
|
19
|
+
export declare const removeFavoriteArtist: (artId: string | number) => Promise<WriteResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Follow someone else's playlist (`playlist.addFavorite`). Note the gateway
|
|
22
|
+
* wants `parent_playlist_id` here but plain `playlist_id` to unfollow.
|
|
23
|
+
*/
|
|
24
|
+
export declare const followPlaylist: (playlistId: string | number) => Promise<WriteResult>;
|
|
25
|
+
/** Unfollow a playlist (`playlist.deleteFavorite`). */
|
|
26
|
+
export declare const unfollowPlaylist: (playlistId: string | number) => Promise<WriteResult>;
|
|
27
|
+
/**
|
|
28
|
+
* Follow a podcast show (`show.addFavorite`).
|
|
29
|
+
*
|
|
30
|
+
* No `show.deleteFavorite` answered the probe, so treat this as one-way until
|
|
31
|
+
* the inverse is found.
|
|
32
|
+
*/
|
|
33
|
+
export declare const addFavoriteShow: (showId: string | number) => Promise<WriteResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Create a playlist (`playlist.create`). Resolves to the new `PLAYLIST_ID`.
|
|
36
|
+
*
|
|
37
|
+
* **One-way.** The gateway has no delete counterpart (see the module note), so
|
|
38
|
+
* anything created here has to be removed from a Deezer client. For the same
|
|
39
|
+
* reason this is the one function on this page that has not been exercised
|
|
40
|
+
* against a live account.
|
|
41
|
+
*
|
|
42
|
+
* @param title playlist title
|
|
43
|
+
* @param options `description`, `status` (0 public / 1 private / 2 collaborative),
|
|
44
|
+
* and `songs` to seed it with `SNG_ID`s
|
|
45
|
+
*/
|
|
46
|
+
export declare const createPlaylist: (title: string, options?: {
|
|
47
|
+
description?: string;
|
|
48
|
+
status?: 0 | 1 | 2;
|
|
49
|
+
songs?: (string | number)[];
|
|
50
|
+
}) => Promise<WriteResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Append tracks to a playlist (`playlist.addSongs`). Reversible with
|
|
53
|
+
* {@link removeTracksFromPlaylist}.
|
|
54
|
+
*
|
|
55
|
+
* The gateway takes `songs` as `[[sngId, offset], …]`; this wraps that for you.
|
|
56
|
+
*/
|
|
57
|
+
export declare const addTracksToPlaylist: (playlistId: string | number, sngIds: (string | number)[]) => Promise<WriteResult>;
|
|
58
|
+
/** Remove tracks from a playlist (`playlist.deleteSongs`). */
|
|
59
|
+
export declare const removeTracksFromPlaylist: (playlistId: string | number, sngIds: (string | number)[]) => Promise<WriteResult>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.removeTracksFromPlaylist = exports.addTracksToPlaylist = exports.createPlaylist = exports.addFavoriteShow = exports.unfollowPlaylist = exports.followPlaylist = exports.removeFavoriteArtist = exports.addFavoriteArtist = exports.removeFavoriteAlbum = exports.addFavoriteAlbum = exports.removeFavoriteTracks = exports.addFavoriteTracks = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* **Mutating** operations on the logged-in account's library.
|
|
6
|
+
*
|
|
7
|
+
* Everything else in this package reads. These change the account, so they are
|
|
8
|
+
* kept in their own module and none of them are wired into any download path —
|
|
9
|
+
* you have to call them deliberately.
|
|
10
|
+
*
|
|
11
|
+
* The method names here were established by probing the gateway with incomplete
|
|
12
|
+
* parameters, which makes an existing method answer `MISSING_PARAMETER_*` while
|
|
13
|
+
* an absent one answers `GATEWAY_ERROR` — so the surface was mapped without
|
|
14
|
+
* creating or changing anything.
|
|
15
|
+
*
|
|
16
|
+
* **Deezer's gateway exposes no way to delete a playlist.** `playlist.delete`,
|
|
17
|
+
* `.remove`, `.destroy` and nine other spellings all answer `GATEWAY_ERROR`.
|
|
18
|
+
* {@link createPlaylist} is therefore a one-way door: whatever it makes, you
|
|
19
|
+
* remove from a Deezer client, not from here. Everything else on this page has a
|
|
20
|
+
* confirmed inverse and is safe to undo.
|
|
21
|
+
*/
|
|
22
|
+
const request_1 = require("./request");
|
|
23
|
+
// ─── Loved tracks ────────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Love one or more tracks (`song.addFavorites`). Reversible with
|
|
26
|
+
* {@link removeFavoriteTracks}.
|
|
27
|
+
*
|
|
28
|
+
* @param sngIds `SNG_ID`s
|
|
29
|
+
*/
|
|
30
|
+
const addFavoriteTracks = (sngIds) => (0, request_1.request)({ ids: sngIds.map(String) }, 'song.addFavorites');
|
|
31
|
+
exports.addFavoriteTracks = addFavoriteTracks;
|
|
32
|
+
/** Un-love one or more tracks (`song.removeFavorites`). */
|
|
33
|
+
const removeFavoriteTracks = (sngIds) => (0, request_1.request)({ ids: sngIds.map(String) }, 'song.removeFavorites');
|
|
34
|
+
exports.removeFavoriteTracks = removeFavoriteTracks;
|
|
35
|
+
// ─── Albums / artists ────────────────────────────────────────────────────────
|
|
36
|
+
/** Add an album to favourites (`album.addFavorite`). */
|
|
37
|
+
const addFavoriteAlbum = (albId) => (0, request_1.request)({ alb_id: String(albId) }, 'album.addFavorite');
|
|
38
|
+
exports.addFavoriteAlbum = addFavoriteAlbum;
|
|
39
|
+
/** Remove an album from favourites (`album.deleteFavorite`). */
|
|
40
|
+
const removeFavoriteAlbum = (albId) => (0, request_1.request)({ alb_id: String(albId) }, 'album.deleteFavorite');
|
|
41
|
+
exports.removeFavoriteAlbum = removeFavoriteAlbum;
|
|
42
|
+
/** Follow an artist (`artist.addFavorite`). */
|
|
43
|
+
const addFavoriteArtist = (artId) => (0, request_1.request)({ art_id: String(artId) }, 'artist.addFavorite');
|
|
44
|
+
exports.addFavoriteArtist = addFavoriteArtist;
|
|
45
|
+
/** Unfollow an artist (`artist.deleteFavorite`). */
|
|
46
|
+
const removeFavoriteArtist = (artId) => (0, request_1.request)({ art_id: String(artId) }, 'artist.deleteFavorite');
|
|
47
|
+
exports.removeFavoriteArtist = removeFavoriteArtist;
|
|
48
|
+
// ─── Playlists & shows ───────────────────────────────────────────────────────
|
|
49
|
+
/**
|
|
50
|
+
* Follow someone else's playlist (`playlist.addFavorite`). Note the gateway
|
|
51
|
+
* wants `parent_playlist_id` here but plain `playlist_id` to unfollow.
|
|
52
|
+
*/
|
|
53
|
+
const followPlaylist = (playlistId) => (0, request_1.request)({ parent_playlist_id: String(playlistId) }, 'playlist.addFavorite');
|
|
54
|
+
exports.followPlaylist = followPlaylist;
|
|
55
|
+
/** Unfollow a playlist (`playlist.deleteFavorite`). */
|
|
56
|
+
const unfollowPlaylist = (playlistId) => (0, request_1.request)({ playlist_id: String(playlistId) }, 'playlist.deleteFavorite');
|
|
57
|
+
exports.unfollowPlaylist = unfollowPlaylist;
|
|
58
|
+
/**
|
|
59
|
+
* Follow a podcast show (`show.addFavorite`).
|
|
60
|
+
*
|
|
61
|
+
* No `show.deleteFavorite` answered the probe, so treat this as one-way until
|
|
62
|
+
* the inverse is found.
|
|
63
|
+
*/
|
|
64
|
+
const addFavoriteShow = (showId) => (0, request_1.request)({ show_id: String(showId) }, 'show.addFavorite');
|
|
65
|
+
exports.addFavoriteShow = addFavoriteShow;
|
|
66
|
+
// ─── Playlist contents ───────────────────────────────────────────────────────
|
|
67
|
+
/**
|
|
68
|
+
* Create a playlist (`playlist.create`). Resolves to the new `PLAYLIST_ID`.
|
|
69
|
+
*
|
|
70
|
+
* **One-way.** The gateway has no delete counterpart (see the module note), so
|
|
71
|
+
* anything created here has to be removed from a Deezer client. For the same
|
|
72
|
+
* reason this is the one function on this page that has not been exercised
|
|
73
|
+
* against a live account.
|
|
74
|
+
*
|
|
75
|
+
* @param title playlist title
|
|
76
|
+
* @param options `description`, `status` (0 public / 1 private / 2 collaborative),
|
|
77
|
+
* and `songs` to seed it with `SNG_ID`s
|
|
78
|
+
*/
|
|
79
|
+
const createPlaylist = (title, options = {}) => {
|
|
80
|
+
var _a, _b, _c;
|
|
81
|
+
return (0, request_1.request)({
|
|
82
|
+
title,
|
|
83
|
+
description: (_a = options.description) !== null && _a !== void 0 ? _a : '',
|
|
84
|
+
status: (_b = options.status) !== null && _b !== void 0 ? _b : 1,
|
|
85
|
+
songs: ((_c = options.songs) !== null && _c !== void 0 ? _c : []).map((id) => [String(id), 0]),
|
|
86
|
+
}, 'playlist.create');
|
|
87
|
+
};
|
|
88
|
+
exports.createPlaylist = createPlaylist;
|
|
89
|
+
/**
|
|
90
|
+
* Append tracks to a playlist (`playlist.addSongs`). Reversible with
|
|
91
|
+
* {@link removeTracksFromPlaylist}.
|
|
92
|
+
*
|
|
93
|
+
* The gateway takes `songs` as `[[sngId, offset], …]`; this wraps that for you.
|
|
94
|
+
*/
|
|
95
|
+
const addTracksToPlaylist = (playlistId, sngIds) => (0, request_1.request)({ playlist_id: String(playlistId), songs: sngIds.map((id) => [String(id), 0]) }, 'playlist.addSongs');
|
|
96
|
+
exports.addTracksToPlaylist = addTracksToPlaylist;
|
|
97
|
+
/** Remove tracks from a playlist (`playlist.deleteSongs`). */
|
|
98
|
+
const removeTracksFromPlaylist = (playlistId, sngIds) => (0, request_1.request)({ playlist_id: String(playlistId), songs: sngIds.map((id) => [String(id), 0]) }, 'playlist.deleteSongs');
|
|
99
|
+
exports.removeTracksFromPlaylist = removeTracksFromPlaylist;
|
package/dist/api/index.d.ts
CHANGED
package/dist/api/index.js
CHANGED
|
@@ -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