gerdur-core 1.0.4 → 2.1.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 +82 -0
- package/README.md +51 -9
- package/dist/api/api.js +4 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/search.d.ts +49 -0
- package/dist/api/search.js +110 -0
- package/dist/lib/metaflac-js.d.ts +1 -1
- package/dist/lib/metaflac-js.js +3 -1
- package/dist/metadata-writer/abumCover.d.ts +9 -5
- package/dist/metadata-writer/abumCover.js +28 -14
- package/dist/metadata-writer/contributors.d.ts +31 -0
- package/dist/metadata-writer/contributors.js +86 -0
- package/dist/metadata-writer/flacmetata.d.ts +11 -2
- package/dist/metadata-writer/flacmetata.js +89 -64
- package/dist/metadata-writer/id3.d.ts +8 -2
- package/dist/metadata-writer/id3.js +110 -103
- package/dist/metadata-writer/index.d.ts +60 -6
- package/dist/metadata-writer/index.js +96 -25
- package/dist/metadata-writer/lrc.d.ts +16 -0
- package/dist/metadata-writer/lrc.js +43 -0
- package/dist/metadata-writer/model.d.ts +75 -0
- package/dist/metadata-writer/model.js +103 -0
- package/dist/metadata-writer/rich-album.d.ts +39 -0
- package/dist/metadata-writer/rich-album.js +53 -0
- package/dist/types/album.d.ts +10 -2
- package/dist/types/search.d.ts +157 -0
- package/dist/types/tracks.d.ts +13 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,87 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.0 - 2026-08-31
|
|
4
|
+
|
|
5
|
+
Phase 2.5 — search, properly. All additive.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`searchPublicApi(query, options?)`** — search the public REST API
|
|
10
|
+
(`api.deezer.com/search`) rather than the internal `pageSearch` gateway.
|
|
11
|
+
Returns clean public-API objects (`isrc`, `preview`, `rank`, numeric ids) and
|
|
12
|
+
takes `type` (`track` | `album` | `artist` | `playlist` | `user` | `radio` |
|
|
13
|
+
`podcast`), `order`, `strict`, and `limit` / `index` paging. No auth required.
|
|
14
|
+
- **`searchTracks` / `searchAlbums` / `searchArtists` / `searchPlaylists`** —
|
|
15
|
+
thin typed wrappers over `searchPublicApi` with the entity fixed.
|
|
16
|
+
- **`buildAdvancedQuery(filters)`** — pure helper that composes Deezer's advanced
|
|
17
|
+
operators into one query string: `{artist, album, track, label}` →
|
|
18
|
+
`artist:"…"`, `{durMin, durMax, bpmMin, bpmMax}` → `dur_min:NNN` / `bpm_min:NNN`,
|
|
19
|
+
free-text `query` first. Reliable on the track index only (Deezer's
|
|
20
|
+
`/search/album` and `/search/artist` ignore the operators).
|
|
21
|
+
- **`suggest(query, nb?)`** — the `deezer.suggest` autocomplete endpoint, for
|
|
22
|
+
"as you type" UIs; cheaper than a full `searchMusic`.
|
|
23
|
+
- New exported types: `advancedSearchFilters`, `searchOrder`, `searchEntity`,
|
|
24
|
+
`publicApiSearchOptions`, `publicApiSearchResponse<T>`, `searchResultTrack`,
|
|
25
|
+
`searchResultAlbum`, `searchResultArtist`, `searchResultPlaylist`,
|
|
26
|
+
`suggestResult`.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **`getPlaylistChannel`** returned `MISSING_PARAMETER_PAGE` — the nested
|
|
31
|
+
`gateway_input` was serialised as `[object Object]`. It is now JSON-stringified
|
|
32
|
+
before the request, so channel pages resolve again.
|
|
33
|
+
|
|
34
|
+
## 2.0.0 - 2026-08-31
|
|
35
|
+
|
|
36
|
+
Metadata overhaul — extract everything Deezer actually exposes for a track.
|
|
37
|
+
|
|
38
|
+
### Breaking
|
|
39
|
+
|
|
40
|
+
- **`addTrackTags(buffer, track, options)`** — the third argument is now an
|
|
41
|
+
options object, not a cover-size number, and it resolves to
|
|
42
|
+
**`{buffer, model}`** instead of a bare `Buffer`. Migrate:
|
|
43
|
+
`await addTrackTags(buf, track, 500)` → `(await addTrackTags(buf, track, {coverSize: 500})).buffer`.
|
|
44
|
+
`model` is the full `TrackTagModel` (all resolved fields + `model.lyricsSynced`,
|
|
45
|
+
an LRC document).
|
|
46
|
+
- `writeMetadataMp3` / `writeMetadataFlac` now take the `TrackTagModel`, not a
|
|
47
|
+
raw track + public-album payload.
|
|
48
|
+
- `downloadAlbumCover` clamps its size to Deezer's real ceiling of **1800**
|
|
49
|
+
(was unbounded); `coverSize` is re-exported from here.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **ReplayGain** — `GAIN` (present on every `song.getData`) is written as
|
|
54
|
+
`REPLAYGAIN_TRACK_GAIN` (`TXXX` on MP3, Vorbis comment on FLAC).
|
|
55
|
+
- **Rich credits** via `normalizeContributors()` — `SNG_CONTRIBUTORS` has
|
|
56
|
+
inconsistent keys across the catalogue (`main_artist` / `mainartist` / `artist`,
|
|
57
|
+
`music publisher` with a space, …); this normalises them and surfaces
|
|
58
|
+
`featuring`, mastering / mixing / recording engineers, producers, mixers,
|
|
59
|
+
performers.
|
|
60
|
+
- **Featured artists** from `SNG_CONTRIBUTORS.featuring` → `TXXX:FEATURING` /
|
|
61
|
+
`FEATURING`, plus a combined `ARTISTS` tag.
|
|
62
|
+
- **Original release date** — `getRichAlbum()` merges gw `album.getData`
|
|
63
|
+
(`ORIGINAL_RELEASE_DATE`, `COPYRIGHT`/`PRODUCER_LINE`, `NUMBER_DISK`,
|
|
64
|
+
`SUBTYPES`) with the public `/album/` (label, genres, record type). Writes
|
|
65
|
+
`ORIGINALDATE`/`ORIGINALYEAR` distinct from the reissue `DATE`.
|
|
66
|
+
- **BPM** from the public `/track/` endpoint (`TBPM` + precise `TXXX:BPM`).
|
|
67
|
+
- **Synced lyrics** — `toLrc()` renders `LYRICS_SYNC_JSON` (already fetched with
|
|
68
|
+
the plain lyrics) as an LRC document, exposed on `model.lyricsSynced` and
|
|
69
|
+
embedded in FLAC `LYRICS`.
|
|
70
|
+
- Real `©` / `℗` lines, disc totals, compilation/live flags from `SUBTYPES`,
|
|
71
|
+
proper `EXPLICIT_LYRICS_STATUS` enum handling (`1`/`4` = explicit; `2`/`6` =
|
|
72
|
+
unknown, not "explicit"), `iTunesAdvisory`, Deezer track/album/artist/label/
|
|
73
|
+
provider ids, `URL_REWRITING` slug, popularity rank, and the artist photo as a
|
|
74
|
+
second embedded picture.
|
|
75
|
+
- New exports: `getRichAlbum`, `RichAlbum`, `normalizeContributors`,
|
|
76
|
+
`NormalizedContributors`, `toLrc`, `buildTagModel`, `TrackTagModel`, `Person`,
|
|
77
|
+
`AddTrackTagsOptions`, `TaggedTrack`, `downloadArtistImage`, `MAX_COVER_SIZE`.
|
|
78
|
+
|
|
79
|
+
### Changed
|
|
80
|
+
|
|
81
|
+
- Album/playlist tracks (which come without `SNG_CONTRIBUTORS` / `VERSION` /
|
|
82
|
+
`GAIN`) are transparently hydrated with one coalesced `song.getData` before
|
|
83
|
+
tagging. Disable via `addTrackTags(…, {richCredits: false})`.
|
|
84
|
+
|
|
3
85
|
## 1.0.4 - 2026-08-31
|
|
4
86
|
|
|
5
87
|
### Added
|
package/README.md
CHANGED
|
@@ -68,11 +68,14 @@ const data = await downloadBuffer(trackData.trackUrl);
|
|
|
68
68
|
// Decrypt track if needed
|
|
69
69
|
const outFile = trackData.isEncrypted ? api.decryptDownload(data, track.SNG_ID) : data;
|
|
70
70
|
|
|
71
|
-
// Add
|
|
72
|
-
const
|
|
71
|
+
// Add metadata — resolves album info, credits, lyrics and cover from Deezer
|
|
72
|
+
const {buffer, model} = await api.addTrackTags(outFile, track, {coverSize: 500});
|
|
73
73
|
|
|
74
74
|
// Save file to disk
|
|
75
|
-
fs.writeFileSync(track.SNG_TITLE + '.mp3',
|
|
75
|
+
fs.writeFileSync(track.SNG_TITLE + '.mp3', buffer);
|
|
76
|
+
|
|
77
|
+
// Time-synced lyrics, when Deezer has them, come back as an LRC document
|
|
78
|
+
if (model.lyricsSynced) fs.writeFileSync(track.SNG_TITLE + '.lrc', model.lyricsSynced);
|
|
76
79
|
```
|
|
77
80
|
|
|
78
81
|
### [Read FAQ](docs/faq.md)
|
|
@@ -159,6 +162,40 @@ All method returns `Object` or throws `Error`. Make sure to catch error on your
|
|
|
159
162
|
| `types` | No | `array` | ['TRACK'] | array of search types |
|
|
160
163
|
| `limit` | No | `number` | 15 | maximum item to fetch per types |
|
|
161
164
|
|
|
165
|
+
### `.searchPublicApi(query, options?)` — and `.searchTracks` / `.searchAlbums` / `.searchArtists` / `.searchPlaylists`
|
|
166
|
+
|
|
167
|
+
Hits the **public** REST API (`api.deezer.com/search`) instead of the internal
|
|
168
|
+
`pageSearch` gateway. Returns clean public-API objects (`isrc`, `preview`,
|
|
169
|
+
`rank`, numeric ids), accepts the advanced query operators, an `order`, and
|
|
170
|
+
`limit` / `index` paging. No auth required.
|
|
171
|
+
|
|
172
|
+
| Parameters | Required | Type | Description |
|
|
173
|
+
| ---------------- | :------: | --------- | --------------------------------------------------------------------------- |
|
|
174
|
+
| `query` | Yes | `string` | plain text, or the output of `buildAdvancedQuery` |
|
|
175
|
+
| `options.type` | No | `string` | `'track'` (default), `'album'`, `'artist'`, `'playlist'`, `'user'`, `'radio'` |
|
|
176
|
+
| `options.order` | No | `string` | `RANKING`, `TRACK_ASC`, `RATING_DESC`, `DURATION_DESC`, … |
|
|
177
|
+
| `options.strict` | No | `boolean` | send Deezer's `strict=on` (disables the fuzzy fallback) |
|
|
178
|
+
| `options.limit` | No | `number` | page size (Deezer caps near 100) |
|
|
179
|
+
| `options.index` | No | `number` | offset into the result set |
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
const {data} = await searchTracks(buildAdvancedQuery({artist: 'daft punk', durMin: 200}), {limit: 25});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### `.buildAdvancedQuery(filters)`
|
|
186
|
+
|
|
187
|
+
Pure helper — composes Deezer's advanced operators into one query string.
|
|
188
|
+
`{artist, album, track, label}` become `artist:"…"`; `{durMin, durMax, bpmMin, bpmMax}`
|
|
189
|
+
become `dur_min:NNN` / `bpm_min:NNN`; a free-text `query` is emitted first.
|
|
190
|
+
Reliable only on the **track** index — `/search/album` and `/search/artist`
|
|
191
|
+
ignore the operators, so pass a plain string there.
|
|
192
|
+
|
|
193
|
+
### `.suggest(query, nb?)`
|
|
194
|
+
|
|
195
|
+
`deezer.suggest` autocomplete — cheaper and faster than `searchMusic`, for
|
|
196
|
+
"as you type" UIs. `nb` (default 5) caps items per type. Needs an initialised
|
|
197
|
+
session (`initDeezerApi`).
|
|
198
|
+
|
|
162
199
|
### `.getTrackDownloadUrl(track, quality);`
|
|
163
200
|
|
|
164
201
|
| Parameters | Required | Type | Description |
|
|
@@ -173,13 +210,18 @@ All method returns `Object` or throws `Error`. Make sure to catch error on your
|
|
|
173
210
|
| `data` | Yes | `buffer` | downloaded song buffer |
|
|
174
211
|
| `song_id` | Yes | `string` | track id |
|
|
175
212
|
|
|
176
|
-
### `.addTrackTags(data, track,
|
|
213
|
+
### `.addTrackTags(data, track, options?)`
|
|
214
|
+
|
|
215
|
+
Resolves album info, credits, lyrics and artwork from Deezer and writes them into
|
|
216
|
+
the audio (ID3v2.3 for MP3, Vorbis comments for FLAC). Returns
|
|
217
|
+
`{buffer, model}` — `model` is the full `TrackTagModel`, including
|
|
218
|
+
`model.lyricsSynced` (an LRC document) when the track has time-synced lyrics.
|
|
177
219
|
|
|
178
|
-
| Parameters
|
|
179
|
-
|
|
|
180
|
-
| `data`
|
|
181
|
-
| `track`
|
|
182
|
-
| `
|
|
220
|
+
| Parameters | Required | Type | Description |
|
|
221
|
+
| ---------- | :------: | -------: | --------------------------------------------- |
|
|
222
|
+
| `data` | Yes | `buffer` | downloaded, decrypted song buffer |
|
|
223
|
+
| `track` | Yes | `object` | track object from `getTrackInfo` / `parseInfo` |
|
|
224
|
+
| `options` | No | `object` | `AddTrackTagsOptions` — `coverSize` (56–1800), pre-fetched `album`/`lyrics`/`cover`, and toggles (`richCredits`, `embedArtistImage`, `deezerIds`, …) |
|
|
183
225
|
|
|
184
226
|
###
|
|
185
227
|
|
package/dist/api/api.js
CHANGED
|
@@ -177,6 +177,9 @@ const getPlaylistChannel = async (page) => {
|
|
|
177
177
|
lang: 'en',
|
|
178
178
|
timezone_offset: '6',
|
|
179
179
|
};
|
|
180
|
-
|
|
180
|
+
// `gateway_input` is a nested object; it must reach the gateway as a JSON
|
|
181
|
+
// string, otherwise it serialises to `[object Object]` and Deezer reports the
|
|
182
|
+
// inner `page` as missing (`MISSING_PARAMETER_PAGE`).
|
|
183
|
+
return await (0, request_1.requestGet)('app_page_get', { gateway_input: JSON.stringify(gateway_input) }, page);
|
|
181
184
|
};
|
|
182
185
|
exports.getPlaylistChannel = getPlaylistChannel;
|
package/dist/api/index.d.ts
CHANGED
package/dist/api/index.js
CHANGED
|
@@ -16,3 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./api"), exports);
|
|
18
18
|
__exportStar(require("./request"), exports);
|
|
19
|
+
__exportStar(require("./search"), exports);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { advancedSearchFilters, publicApiSearchOptions, publicApiSearchResponse, searchResultAlbum, searchResultArtist, searchResultPlaylist, searchResultTrack, suggestResult } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Compose Deezer's advanced search operators into one query string.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* buildAdvancedQuery({artist: 'daft punk', durMin: 200, durMax: 400});
|
|
7
|
+
* // => 'artist:"daft punk" dur_min:200 dur_max:400'
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* Free-text `query` is emitted first and bare. Deezer treats the operators as
|
|
11
|
+
* ranking hints rather than hard filters (and honours `track:` only now and
|
|
12
|
+
* then) — this only builds the string their docs describe. Pair it with
|
|
13
|
+
* `searchPublicApi(query, {strict: true})` to tighten what Deezer allows.
|
|
14
|
+
*
|
|
15
|
+
* The operators are reliable only on the **track** index (`searchPublicApi` /
|
|
16
|
+
* `searchTracks`). `/search/album` and `/search/artist` ignore or mishandle
|
|
17
|
+
* `artist:` / `album:` — use a plain string there.
|
|
18
|
+
*/
|
|
19
|
+
export declare const buildAdvancedQuery: (filters: advancedSearchFilters) => string;
|
|
20
|
+
/**
|
|
21
|
+
* Search the **public** Deezer REST API (`api.deezer.com/search`).
|
|
22
|
+
*
|
|
23
|
+
* Unlike `searchMusic` (which drives the internal `deezer.pageSearch` gateway),
|
|
24
|
+
* this returns clean public-API objects — with `isrc`, `preview`, `rank`, real
|
|
25
|
+
* numeric ids — and accepts the advanced query operators (see
|
|
26
|
+
* `buildAdvancedQuery`), an `order`, and `limit` / `index` paging. No auth
|
|
27
|
+
* required; results are memoised by the api layer.
|
|
28
|
+
*
|
|
29
|
+
* @param query a plain string, or the output of `buildAdvancedQuery`
|
|
30
|
+
* @param options `type` (`'track'` default), `order`, `strict`, `limit`, `index`
|
|
31
|
+
*/
|
|
32
|
+
export declare const searchPublicApi: <T = searchResultTrack>(query: string, options?: publicApiSearchOptions) => Promise<publicApiSearchResponse<T>>;
|
|
33
|
+
/** `searchPublicApi` fixed to tracks. */
|
|
34
|
+
export declare const searchTracks: (query: string, options?: Omit<publicApiSearchOptions, 'type'>) => Promise<publicApiSearchResponse<searchResultTrack>>;
|
|
35
|
+
/** `searchPublicApi` fixed to albums. */
|
|
36
|
+
export declare const searchAlbums: (query: string, options?: Omit<publicApiSearchOptions, 'type'>) => Promise<publicApiSearchResponse<searchResultAlbum>>;
|
|
37
|
+
/** `searchPublicApi` fixed to artists. */
|
|
38
|
+
export declare const searchArtists: (query: string, options?: Omit<publicApiSearchOptions, 'type'>) => Promise<publicApiSearchResponse<searchResultArtist>>;
|
|
39
|
+
/** `searchPublicApi` fixed to playlists. */
|
|
40
|
+
export declare const searchPlaylists: (query: string, options?: Omit<publicApiSearchOptions, 'type'>) => Promise<publicApiSearchResponse<searchResultPlaylist>>;
|
|
41
|
+
/**
|
|
42
|
+
* `deezer.suggest` — the internal autocomplete endpoint. Cheaper and faster than
|
|
43
|
+
* a full `searchMusic`, and it powers "as you type" suggestion UIs. Needs an
|
|
44
|
+
* initialised session (`initDeezerApi`), like every gateway call.
|
|
45
|
+
*
|
|
46
|
+
* @param query partial query text
|
|
47
|
+
* @param nb max items per type (default 5)
|
|
48
|
+
*/
|
|
49
|
+
export declare const suggest: (query: string, nb?: number) => Promise<suggestResult>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.suggest = exports.searchPlaylists = exports.searchArtists = exports.searchAlbums = exports.searchTracks = exports.searchPublicApi = exports.buildAdvancedQuery = void 0;
|
|
4
|
+
const request_1 = require("./request");
|
|
5
|
+
/**
|
|
6
|
+
* Compose Deezer's advanced search operators into one query string.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* buildAdvancedQuery({artist: 'daft punk', durMin: 200, durMax: 400});
|
|
10
|
+
* // => 'artist:"daft punk" dur_min:200 dur_max:400'
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Free-text `query` is emitted first and bare. Deezer treats the operators as
|
|
14
|
+
* ranking hints rather than hard filters (and honours `track:` only now and
|
|
15
|
+
* then) — this only builds the string their docs describe. Pair it with
|
|
16
|
+
* `searchPublicApi(query, {strict: true})` to tighten what Deezer allows.
|
|
17
|
+
*
|
|
18
|
+
* The operators are reliable only on the **track** index (`searchPublicApi` /
|
|
19
|
+
* `searchTracks`). `/search/album` and `/search/artist` ignore or mishandle
|
|
20
|
+
* `artist:` / `album:` — use a plain string there.
|
|
21
|
+
*/
|
|
22
|
+
const buildAdvancedQuery = (filters) => {
|
|
23
|
+
const { query, artist, album, track, label, durMin, durMax, bpmMin, bpmMax } = filters;
|
|
24
|
+
const parts = [];
|
|
25
|
+
if (query && query.trim()) {
|
|
26
|
+
parts.push(query.trim());
|
|
27
|
+
}
|
|
28
|
+
const textOps = [
|
|
29
|
+
['artist', artist],
|
|
30
|
+
['album', album],
|
|
31
|
+
['track', track],
|
|
32
|
+
['label', label],
|
|
33
|
+
];
|
|
34
|
+
for (const [op, value] of textOps) {
|
|
35
|
+
if (value && value.trim()) {
|
|
36
|
+
parts.push(`${op}:"${value.trim().replace(/"/g, '')}"`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const rangeOps = [
|
|
40
|
+
['dur_min', durMin],
|
|
41
|
+
['dur_max', durMax],
|
|
42
|
+
['bpm_min', bpmMin],
|
|
43
|
+
['bpm_max', bpmMax],
|
|
44
|
+
];
|
|
45
|
+
for (const [op, value] of rangeOps) {
|
|
46
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
|
|
47
|
+
parts.push(`${op}:${Math.round(value)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return parts.join(' ');
|
|
51
|
+
};
|
|
52
|
+
exports.buildAdvancedQuery = buildAdvancedQuery;
|
|
53
|
+
const SEARCH_ENTITIES = ['track', 'album', 'artist', 'playlist', 'user', 'radio', 'podcast'];
|
|
54
|
+
/**
|
|
55
|
+
* Search the **public** Deezer REST API (`api.deezer.com/search`).
|
|
56
|
+
*
|
|
57
|
+
* Unlike `searchMusic` (which drives the internal `deezer.pageSearch` gateway),
|
|
58
|
+
* this returns clean public-API objects — with `isrc`, `preview`, `rank`, real
|
|
59
|
+
* numeric ids — and accepts the advanced query operators (see
|
|
60
|
+
* `buildAdvancedQuery`), an `order`, and `limit` / `index` paging. No auth
|
|
61
|
+
* required; results are memoised by the api layer.
|
|
62
|
+
*
|
|
63
|
+
* @param query a plain string, or the output of `buildAdvancedQuery`
|
|
64
|
+
* @param options `type` (`'track'` default), `order`, `strict`, `limit`, `index`
|
|
65
|
+
*/
|
|
66
|
+
const searchPublicApi = (query, options = {}) => {
|
|
67
|
+
const { type, order, strict, limit, index } = options;
|
|
68
|
+
const segment = type && type !== 'track' && SEARCH_ENTITIES.includes(type) ? `/${type}` : '';
|
|
69
|
+
const search = new URLSearchParams({ q: query });
|
|
70
|
+
if (strict) {
|
|
71
|
+
search.set('strict', 'on');
|
|
72
|
+
}
|
|
73
|
+
if (order) {
|
|
74
|
+
search.set('order', order);
|
|
75
|
+
}
|
|
76
|
+
if (typeof limit === 'number') {
|
|
77
|
+
search.set('limit', String(limit));
|
|
78
|
+
}
|
|
79
|
+
if (typeof index === 'number') {
|
|
80
|
+
search.set('index', String(index));
|
|
81
|
+
}
|
|
82
|
+
return (0, request_1.requestPublicApi)(`/search${segment}?${search.toString()}`);
|
|
83
|
+
};
|
|
84
|
+
exports.searchPublicApi = searchPublicApi;
|
|
85
|
+
/** `searchPublicApi` fixed to tracks. */
|
|
86
|
+
const searchTracks = (query, options = {}) => (0, exports.searchPublicApi)(query, { ...options, type: 'track' });
|
|
87
|
+
exports.searchTracks = searchTracks;
|
|
88
|
+
/** `searchPublicApi` fixed to albums. */
|
|
89
|
+
const searchAlbums = (query, options = {}) => (0, exports.searchPublicApi)(query, { ...options, type: 'album' });
|
|
90
|
+
exports.searchAlbums = searchAlbums;
|
|
91
|
+
/** `searchPublicApi` fixed to artists. */
|
|
92
|
+
const searchArtists = (query, options = {}) => (0, exports.searchPublicApi)(query, { ...options, type: 'artist' });
|
|
93
|
+
exports.searchArtists = searchArtists;
|
|
94
|
+
/** `searchPublicApi` fixed to playlists. */
|
|
95
|
+
const searchPlaylists = (query, options = {}) => (0, exports.searchPublicApi)(query, { ...options, type: 'playlist' });
|
|
96
|
+
exports.searchPlaylists = searchPlaylists;
|
|
97
|
+
/**
|
|
98
|
+
* `deezer.suggest` — the internal autocomplete endpoint. Cheaper and faster than
|
|
99
|
+
* a full `searchMusic`, and it powers "as you type" suggestion UIs. Needs an
|
|
100
|
+
* initialised session (`initDeezerApi`), like every gateway call.
|
|
101
|
+
*
|
|
102
|
+
* @param query partial query text
|
|
103
|
+
* @param nb max items per type (default 5)
|
|
104
|
+
*/
|
|
105
|
+
const suggest = (query, nb = 5) => (0, request_1.requestLight)({
|
|
106
|
+
QUERY: query,
|
|
107
|
+
NB: nb,
|
|
108
|
+
TYPES: { ALBUM: true, ARTIST: true, TRACK: true, PLAYLIST: true, RADIO: true, SHOW: true },
|
|
109
|
+
}, 'deezer.suggest');
|
|
110
|
+
exports.suggest = suggest;
|
|
@@ -91,7 +91,7 @@ declare class Metaflac {
|
|
|
91
91
|
*
|
|
92
92
|
* @param {string} filename
|
|
93
93
|
*/
|
|
94
|
-
importPicture(picture: Buffer, dimension: number, mime: 'image/jpeg' | 'image/png'): void;
|
|
94
|
+
importPicture(picture: Buffer, dimension: number, mime: 'image/jpeg' | 'image/png', type?: number, description?: string): void;
|
|
95
95
|
/**
|
|
96
96
|
* Return all tags.
|
|
97
97
|
*/
|
package/dist/lib/metaflac-js.js
CHANGED
|
@@ -236,9 +236,11 @@ class Metaflac {
|
|
|
236
236
|
*
|
|
237
237
|
* @param {string} filename
|
|
238
238
|
*/
|
|
239
|
-
importPicture(picture, dimension, mime) {
|
|
239
|
+
importPicture(picture, dimension, mime, type = 3, description = '') {
|
|
240
240
|
const spec = this.buildSpecification({
|
|
241
|
+
type,
|
|
241
242
|
mime,
|
|
243
|
+
description,
|
|
242
244
|
width: dimension,
|
|
243
245
|
height: dimension,
|
|
244
246
|
});
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import type { trackType } from '../types';
|
|
3
|
-
type coverSize = 56 | 250 | 500 | 1000 | 1500 | 1800 | number;
|
|
3
|
+
export type coverSize = 56 | 250 | 500 | 1000 | 1200 | 1500 | 1800 | number;
|
|
4
|
+
export declare const MAX_COVER_SIZE = 1800;
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
* @param
|
|
7
|
-
* @param {Number} albumCoverSize in pixel, between 56-1800
|
|
6
|
+
* @param track track info json from deezer api
|
|
7
|
+
* @param albumCoverSize in pixels, 56–1800 (clamped)
|
|
8
8
|
*/
|
|
9
9
|
export declare const downloadAlbumCover: (track: trackType, albumCoverSize: coverSize) => Promise<Buffer | null>;
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Artist photo (`ART_PICTURE` md5). Returns null when the track has no artist
|
|
12
|
+
* image (common for small-catalogue artists).
|
|
13
|
+
*/
|
|
14
|
+
export declare const downloadArtistImage: (track: trackType, size?: coverSize) => Promise<Buffer | null>;
|
|
@@ -3,35 +3,49 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.downloadAlbumCover = void 0;
|
|
6
|
+
exports.downloadArtistImage = exports.downloadAlbumCover = exports.MAX_COVER_SIZE = void 0;
|
|
7
7
|
const fast_lru_1 = __importDefault(require("../lib/fast-lru"));
|
|
8
8
|
const http_1 = require("../lib/http");
|
|
9
|
+
exports.MAX_COVER_SIZE = 1800;
|
|
9
10
|
// expire cache in 30 minutes
|
|
10
11
|
const lru = new fast_lru_1.default({
|
|
11
12
|
maxSize: 50,
|
|
12
13
|
ttl: 30 * 60000,
|
|
13
14
|
});
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*/
|
|
19
|
-
const downloadAlbumCover = async (track, albumCoverSize) => {
|
|
20
|
-
if (!track.ALB_PICTURE) {
|
|
15
|
+
const clampSize = (n) => Math.max(56, Math.min(exports.MAX_COVER_SIZE, Math.round(n) || 500));
|
|
16
|
+
const imageUrl = (kind, md5, size) => `https://e-cdns-images.dzcdn.net/images/${kind}/${md5}/${size}x${size}-000000-80-0-0.jpg`;
|
|
17
|
+
const fetchImage = async (kind, md5, size) => {
|
|
18
|
+
if (!md5) {
|
|
21
19
|
return null;
|
|
22
20
|
}
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
const px = clampSize(size);
|
|
22
|
+
const key = `${kind}:${md5}:${px}`;
|
|
23
|
+
const cached = lru.get(key);
|
|
24
|
+
if (cached) {
|
|
25
|
+
return cached;
|
|
26
26
|
}
|
|
27
27
|
try {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
lru.set(track.ALB_PICTURE + albumCoverSize, data);
|
|
28
|
+
const data = await (0, http_1.getBuffer)(imageUrl(kind, md5, px));
|
|
29
|
+
lru.set(key, data);
|
|
31
30
|
return data;
|
|
32
31
|
}
|
|
33
32
|
catch (err) {
|
|
34
33
|
return null;
|
|
35
34
|
}
|
|
36
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* @param track track info json from deezer api
|
|
38
|
+
* @param albumCoverSize in pixels, 56–1800 (clamped)
|
|
39
|
+
*/
|
|
40
|
+
const downloadAlbumCover = (track, albumCoverSize) => fetchImage('cover', track.ALB_PICTURE, albumCoverSize);
|
|
37
41
|
exports.downloadAlbumCover = downloadAlbumCover;
|
|
42
|
+
/**
|
|
43
|
+
* Artist photo (`ART_PICTURE` md5). Returns null when the track has no artist
|
|
44
|
+
* image (common for small-catalogue artists).
|
|
45
|
+
*/
|
|
46
|
+
const downloadArtistImage = (track, size = 1000) => {
|
|
47
|
+
var _a, _b;
|
|
48
|
+
const md5 = track.ART_PICTURE || ((_b = (_a = track.ARTISTS) === null || _a === void 0 ? void 0 : _a.find((a) => a.ART_PICTURE)) === null || _b === void 0 ? void 0 : _b.ART_PICTURE);
|
|
49
|
+
return fetchImage('artist', md5, size);
|
|
50
|
+
};
|
|
51
|
+
exports.downloadArtistImage = downloadArtistImage;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { sngContributors } from '../types';
|
|
2
|
+
export interface NormalizedContributors {
|
|
3
|
+
/** billed / lead artists */
|
|
4
|
+
mainArtists: string[];
|
|
5
|
+
/** "feat." artists */
|
|
6
|
+
featuring: string[];
|
|
7
|
+
composers: string[];
|
|
8
|
+
/** author / writer / lyricist, de-duplicated */
|
|
9
|
+
lyricists: string[];
|
|
10
|
+
producers: string[];
|
|
11
|
+
/** {role, name} for mastering / mixing / recording (+ "second") engineers */
|
|
12
|
+
engineers: {
|
|
13
|
+
role: string;
|
|
14
|
+
name: string;
|
|
15
|
+
}[];
|
|
16
|
+
mixers: string[];
|
|
17
|
+
/** other performers Deezer credits by role, e.g. background vocalist */
|
|
18
|
+
performers: {
|
|
19
|
+
role: string;
|
|
20
|
+
name: string;
|
|
21
|
+
}[];
|
|
22
|
+
publishers: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Turn Deezer's inconsistent `SNG_CONTRIBUTORS` bag into a stable shape.
|
|
26
|
+
*
|
|
27
|
+
* The role keys vary across the catalogue (`main_artist` / `mainartist` /
|
|
28
|
+
* `artist`, `musicpublisher` / `music publisher`, …) and the value is
|
|
29
|
+
* occasionally an empty array — this hides all of that.
|
|
30
|
+
*/
|
|
31
|
+
export declare const normalizeContributors: (raw: sngContributors | undefined) => NormalizedContributors;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeContributors = void 0;
|
|
4
|
+
/** collapse `Music Publisher`, `music_publisher`, `musicpublisher` → `musicpublisher` */
|
|
5
|
+
const canon = (key) => key.toLowerCase().replace(/[\s_-]+/g, '');
|
|
6
|
+
const ENGINEER_LABELS = {
|
|
7
|
+
masteringengineer: 'mastering engineer',
|
|
8
|
+
mixingengineer: 'mixing engineer',
|
|
9
|
+
recordingengineer: 'recording engineer',
|
|
10
|
+
recordingsecondengineer: 'assistant recording engineer',
|
|
11
|
+
assistantengineer: 'assistant engineer',
|
|
12
|
+
engineer: 'engineer',
|
|
13
|
+
studiopersonnel: 'engineer',
|
|
14
|
+
};
|
|
15
|
+
const uniq = (arr) => [...new Set(arr.filter((x) => x && x.trim()))].map((x) => x.trim());
|
|
16
|
+
/**
|
|
17
|
+
* Turn Deezer's inconsistent `SNG_CONTRIBUTORS` bag into a stable shape.
|
|
18
|
+
*
|
|
19
|
+
* The role keys vary across the catalogue (`main_artist` / `mainartist` /
|
|
20
|
+
* `artist`, `musicpublisher` / `music publisher`, …) and the value is
|
|
21
|
+
* occasionally an empty array — this hides all of that.
|
|
22
|
+
*/
|
|
23
|
+
const normalizeContributors = (raw) => {
|
|
24
|
+
const out = {
|
|
25
|
+
mainArtists: [],
|
|
26
|
+
featuring: [],
|
|
27
|
+
composers: [],
|
|
28
|
+
lyricists: [],
|
|
29
|
+
producers: [],
|
|
30
|
+
engineers: [],
|
|
31
|
+
mixers: [],
|
|
32
|
+
performers: [],
|
|
33
|
+
publishers: [],
|
|
34
|
+
};
|
|
35
|
+
if (!raw || Array.isArray(raw)) {
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
const bucket = {};
|
|
39
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
40
|
+
if (!Array.isArray(value)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const k = canon(key);
|
|
44
|
+
bucket[k] = (bucket[k] || []).concat(value);
|
|
45
|
+
}
|
|
46
|
+
out.mainArtists = uniq([...(bucket.mainartist || []), ...(bucket.artist || [])]);
|
|
47
|
+
out.featuring = uniq([...(bucket.featuring || []), ...(bucket.featuredartist || []), ...(bucket.feat || [])]);
|
|
48
|
+
out.composers = uniq(bucket.composer || []);
|
|
49
|
+
out.lyricists = uniq([
|
|
50
|
+
...(bucket.author || []),
|
|
51
|
+
...(bucket.writer || []),
|
|
52
|
+
...(bucket.lyricist || []),
|
|
53
|
+
...(bucket.songwriter || []),
|
|
54
|
+
]);
|
|
55
|
+
out.producers = uniq([...(bucket.producer || []), ...(bucket.coproducer || []), ...(bucket.executiveproducer || [])]);
|
|
56
|
+
out.mixers = uniq([...(bucket.mixer || []), ...(bucket.remixer || [])]);
|
|
57
|
+
out.publishers = uniq([
|
|
58
|
+
...(bucket.publisher || []),
|
|
59
|
+
...(bucket.musicpublisher || []),
|
|
60
|
+
...(bucket.originalpublisher || []),
|
|
61
|
+
]);
|
|
62
|
+
for (const [canonKey, label] of Object.entries(ENGINEER_LABELS)) {
|
|
63
|
+
for (const name of uniq(bucket[canonKey] || [])) {
|
|
64
|
+
out.engineers.push({ role: label, name });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const [canonKey, names] of Object.entries(bucket)) {
|
|
68
|
+
if (canonKey === 'mainartist' ||
|
|
69
|
+
canonKey === 'artist' ||
|
|
70
|
+
canonKey === 'featuring' ||
|
|
71
|
+
canonKey === 'composer' ||
|
|
72
|
+
canonKey === 'mixer' ||
|
|
73
|
+
canonKey === 'producer' ||
|
|
74
|
+
canonKey in ENGINEER_LABELS ||
|
|
75
|
+
/publisher|author|writer|lyricist/.test(canonKey)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// remaining roles: vocalist, backgroundvocalist, instruments, arranger, …
|
|
79
|
+
const role = canonKey.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
80
|
+
for (const name of uniq(names)) {
|
|
81
|
+
out.performers.push({ role, name });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
};
|
|
86
|
+
exports.normalizeContributors = normalizeContributors;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
import type {
|
|
3
|
-
export
|
|
2
|
+
import type { TrackTagModel } from './model';
|
|
3
|
+
export interface FlacWriteOptions {
|
|
4
|
+
/** embed the LRC document as a `SYNCEDLYRICS` Vorbis comment. default true */
|
|
5
|
+
embedSyncedLyrics?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Write Vorbis comments + PICTURE blocks from the canonical model. Vorbis
|
|
9
|
+
* comments are free-form, so every field Deezer gives us lands here — including
|
|
10
|
+
* multi-valued credits (one comment per value) and synced lyrics.
|
|
11
|
+
*/
|
|
12
|
+
export declare const writeMetadataFlac: (buffer: Buffer, m: TrackTagModel, options?: FlacWriteOptions) => Buffer;
|