gerdur 1.0.2 → 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 +40 -0
- package/README.md +19 -7
- package/dist/package.json +2 -2
- package/dist/src/gerdur.js +2 -0
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +14 -1
- package/dist/src/lib/api-download.d.ts +19 -2
- package/dist/src/lib/api-download.js +39 -19
- package/dist/src/lib/config.d.ts +1 -1
- package/dist/src/lib/config.js +6 -0
- package/dist/src/lib/download-track.d.ts +3 -1
- package/dist/src/lib/download-track.js +7 -2
- package/dist/src/lib/session.d.ts +12 -1
- package/dist/src/lib/session.js +15 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.0 - 2026-08-31
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`gerdur-core@^2.1.0`** — the new search surface is re-exported from the
|
|
8
|
+
library: `searchPublicApi`, `searchTracks` / `searchAlbums` / `searchArtists` /
|
|
9
|
+
`searchPlaylists`, `buildAdvancedQuery`, `suggest`, plus the matching types.
|
|
10
|
+
- **`Session.searchAdvanced(filters, options?)`** — structured search against the
|
|
11
|
+
public REST API — a free-text query plus `{artist, album, track, label, durMin,
|
|
12
|
+
durMax, bpmMin, bpmMax}`, with `order` / `limit` / `index`. Returns public-API
|
|
13
|
+
track objects with `isrc` / `preview`; fetch a hit's gw track with
|
|
14
|
+
`getTrackInfo(id)` to download.
|
|
15
|
+
- **`Session.suggest(query, nb?)`** — `deezer.suggest` autocomplete.
|
|
16
|
+
|
|
17
|
+
_No CLI changes in this release — the interactive `gerdur` flow is unchanged._
|
|
18
|
+
|
|
19
|
+
## 2.0.0 - 2026-08-31
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **Metadata is now much richer** (`gerdur-core@^2.0.0`). Every downloaded file
|
|
24
|
+
gets ReplayGain (`REPLAYGAIN_TRACK_GAIN`), BPM, real `©`/`℗` lines, the true
|
|
25
|
+
original release date (distinct from a reissue date), full studio credits
|
|
26
|
+
(featured artists, mastering / mixing / recording engineers, producers),
|
|
27
|
+
compilation/live flags, iTunes advisory, Deezer ids, and the artist photo as a
|
|
28
|
+
second embedded image. Album/playlist tracks are hydrated with one coalesced
|
|
29
|
+
`song.getData` so the credits are complete.
|
|
30
|
+
- **`.lrc` sidecar files** are written next to the audio for tracks with
|
|
31
|
+
time-synced lyrics. Toggle with `"lyrics": {"lrcFile": false}` in
|
|
32
|
+
`gerdur.config.json`.
|
|
33
|
+
- Cover-art requests are capped at Deezer's real ceiling of 1800 px.
|
|
34
|
+
|
|
35
|
+
### Breaking (programmatic API)
|
|
36
|
+
|
|
37
|
+
- `gerdur-core.addTrackTags` changed signature/return — see its 2.0.0 changelog.
|
|
38
|
+
`getTrackBuffer` is unaffected (still resolves to `Buffer | null`); new
|
|
39
|
+
`getTaggedTrack(track, quality, options)` returns `{buffer, model}` with the
|
|
40
|
+
structured metadata and LRC. `downloadTrackToFile` now also returns `lrcPath`
|
|
41
|
+
when a sidecar was written.
|
|
42
|
+
|
|
3
43
|
## 1.0.1 - 2026-08-30
|
|
4
44
|
|
|
5
45
|
### Changed
|
package/README.md
CHANGED
|
@@ -175,12 +175,22 @@ await session.downloadUrl('https://deezer.com/album/302127', 'flac', {
|
|
|
175
175
|
const {tracks} = await session.parseUrl('https://deezer.com/track/3135556');
|
|
176
176
|
const found = await session.search('daft punk', ['TRACK'], 10);
|
|
177
177
|
const results = await session.downloadTracks(tracks, '320', {output: '{ART_NAME} - {SNG_TITLE}'});
|
|
178
|
+
|
|
179
|
+
// Structured search against the public REST API (isrc / preview / bpm-aware):
|
|
180
|
+
const hits = await session.searchAdvanced(
|
|
181
|
+
{query: 'one more time', artist: 'daft punk', durMin: 200},
|
|
182
|
+
{limit: 25, order: 'RANKING'},
|
|
183
|
+
);
|
|
184
|
+
const suggestions = await session.suggest('daft'); // autocomplete
|
|
178
185
|
```
|
|
179
186
|
|
|
180
|
-
Session methods: `parseUrl`, `search`, `
|
|
181
|
-
`downloadTracks`, `downloadUrl`. Every download
|
|
182
|
-
`downloadUrl` accept `concurrency` and an
|
|
183
|
-
`{path, written} | null` per track.
|
|
187
|
+
Session methods: `parseUrl`, `search`, `searchAdvanced`, `suggest`, `getUser`,
|
|
188
|
+
`getTrackBuffer`, `downloadTrack`, `downloadTracks`, `downloadUrl`. Every download
|
|
189
|
+
call is silent; `downloadTracks` / `downloadUrl` accept `concurrency` and an
|
|
190
|
+
`onProgress` callback and return one `{path, written} | null` per track.
|
|
191
|
+
|
|
192
|
+
`searchAdvanced` returns public-API track objects; fetch a hit's gw track with
|
|
193
|
+
`getTrackInfo(id)` before downloading it.
|
|
184
194
|
|
|
185
195
|
### Low-level: primitives
|
|
186
196
|
|
|
@@ -204,9 +214,11 @@ const {path, written} = (await downloadTrackToFile(track, 'flac', {output: '{ART
|
|
|
204
214
|
So you don't need `gerdur-core` as a second dependency (call after `initDeezerApi`
|
|
205
215
|
or `createSession`):
|
|
206
216
|
|
|
207
|
-
`parseInfo`, `searchMusic`, `
|
|
208
|
-
`
|
|
209
|
-
`
|
|
217
|
+
`parseInfo`, `searchMusic`, `searchPublicApi`, `searchTracks`, `searchAlbums`,
|
|
218
|
+
`searchArtists`, `searchPlaylists`, `buildAdvancedQuery`, `suggest`, `getUser`,
|
|
219
|
+
`getTrackInfo`, `getAlbumInfo`, `getAlbumTracks`, `getPlaylistInfo`,
|
|
220
|
+
`getPlaylistTracks`, `getArtistInfo`, `getDiscography`, `getLyrics`,
|
|
221
|
+
`getTrackDownloadUrl`, `resolveDownloadUrls`, `GeoBlocked`.
|
|
210
222
|
|
|
211
223
|
### Auth & config helpers
|
|
212
224
|
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gerdur",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "The crucial streaming module for dabox systems.",
|
|
5
5
|
"author": "Christian",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"adm-zip": "^0.5.16",
|
|
49
49
|
"chalk": "^4.1.2",
|
|
50
50
|
"commander": "^9.5.0",
|
|
51
|
-
"gerdur-core": "^1.0
|
|
51
|
+
"gerdur-core": "^2.1.0",
|
|
52
52
|
"dot-prop": "^6.0.1",
|
|
53
53
|
"got": "^11.8.6",
|
|
54
54
|
"gradient-string": "^2.0.2",
|
package/dist/src/gerdur.js
CHANGED
|
@@ -215,6 +215,7 @@ const startDownload = async (saveLayout, url, skipPrompt) => {
|
|
|
215
215
|
const trackNumber = conf.get('trackNumber', true);
|
|
216
216
|
const fallbackTrack = conf.get('fallbackTrack', true);
|
|
217
217
|
const fallbackQuality = conf.get('fallbackQuality', true);
|
|
218
|
+
const lrc = conf.get('lyrics.lrcFile', true);
|
|
218
219
|
const overwrite = options.overwrite ?? conf.get('overwrite', false);
|
|
219
220
|
const resolveFullPath = options.resolveFullPath ?? conf.get('playlist.resolveFullPath');
|
|
220
221
|
const savedFiles = [];
|
|
@@ -259,6 +260,7 @@ const startDownload = async (saveLayout, url, skipPrompt) => {
|
|
|
259
260
|
fallbackQuality,
|
|
260
261
|
overwrite,
|
|
261
262
|
message: `(${index}/${data.tracks.length})`,
|
|
263
|
+
lrc,
|
|
262
264
|
prefetched: prefetchedUrls.get(track.SNG_ID) ?? null,
|
|
263
265
|
});
|
|
264
266
|
// Add to saved list
|
package/dist/src/index.d.ts
CHANGED
|
@@ -56,7 +56,9 @@ export declare class LoginError extends Error {
|
|
|
56
56
|
export declare const getArl: (email: string, password: string) => Promise<string>;
|
|
57
57
|
export { createSession, Session } from './lib/session';
|
|
58
58
|
export type { SessionOptions, DownloadTracksOptions, SearchType } from './lib/session';
|
|
59
|
-
export { getTrackBuffer, downloadTrackToFile } from './lib/api-download';
|
|
59
|
+
export { getTrackBuffer, getTaggedTrack, downloadTrackToFile } from './lib/api-download';
|
|
60
60
|
export type { Quality, GetTrackBufferOptions, DownloadTrackOptions, DownloadResult } from './lib/api-download';
|
|
61
61
|
export { default as Config, globalConfigPath, resolveConfigFile } from './lib/config';
|
|
62
|
-
export { initDeezerApi, parseInfo, searchMusic, getUser, getTrackInfo, getAlbumInfo, getAlbumTracks, getPlaylistInfo, getPlaylistTracks, getArtistInfo, getDiscography, getLyrics, getTrackDownloadUrl, GeoBlocked, } from 'gerdur-core';
|
|
62
|
+
export { initDeezerApi, parseInfo, searchMusic, searchPublicApi, searchTracks, searchAlbums, searchArtists, searchPlaylists, buildAdvancedQuery, suggest, getUser, getTrackInfo, getAlbumInfo, getAlbumTracks, getPlaylistInfo, getPlaylistTracks, getArtistInfo, getDiscography, getLyrics, getTrackDownloadUrl, resolveDownloadUrls, addTrackTags, getRichAlbum, normalizeContributors, toLrc, GeoBlocked, } from 'gerdur-core';
|
|
63
|
+
export type { AddTrackTagsOptions, TaggedTrack, TrackTagModel, RichAlbum, ResolvedUrl, NormalizedContributors, } from 'gerdur-core';
|
|
64
|
+
export type { advancedSearchFilters, searchOrder, searchEntity, publicApiSearchOptions, publicApiSearchResponse, searchResultTrack, searchResultAlbum, searchResultArtist, searchResultPlaylist, suggestResult, } from 'gerdur-core/types';
|
package/dist/src/index.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
33
33
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
34
34
|
};
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.GeoBlocked = exports.getTrackDownloadUrl = exports.getLyrics = exports.getDiscography = exports.getArtistInfo = exports.getPlaylistTracks = exports.getPlaylistInfo = exports.getAlbumTracks = exports.getAlbumInfo = exports.getTrackInfo = exports.getUser = exports.searchMusic = exports.parseInfo = exports.initDeezerApi = exports.resolveConfigFile = exports.globalConfigPath = exports.Config = exports.downloadTrackToFile = exports.getTrackBuffer = exports.Session = exports.createSession = exports.getArl = exports.LoginError = exports.loginWithEmail = void 0;
|
|
36
|
+
exports.GeoBlocked = exports.toLrc = exports.normalizeContributors = exports.getRichAlbum = exports.addTrackTags = exports.resolveDownloadUrls = exports.getTrackDownloadUrl = exports.getLyrics = exports.getDiscography = exports.getArtistInfo = exports.getPlaylistTracks = exports.getPlaylistInfo = exports.getAlbumTracks = exports.getAlbumInfo = exports.getTrackInfo = exports.getUser = exports.suggest = exports.buildAdvancedQuery = exports.searchPlaylists = exports.searchArtists = exports.searchAlbums = exports.searchTracks = exports.searchPublicApi = exports.searchMusic = exports.parseInfo = exports.initDeezerApi = exports.resolveConfigFile = exports.globalConfigPath = exports.Config = exports.downloadTrackToFile = exports.getTaggedTrack = exports.getTrackBuffer = exports.Session = exports.createSession = exports.getArl = exports.LoginError = exports.loginWithEmail = void 0;
|
|
37
37
|
// ─── Authentication ────────────────────────────────────────────────────────
|
|
38
38
|
const email_login_1 = require("./lib/email-login");
|
|
39
39
|
Object.defineProperty(exports, "loginWithEmail", { enumerable: true, get: function () { return email_login_1.loginWithEmail; } });
|
|
@@ -74,6 +74,7 @@ Object.defineProperty(exports, "Session", { enumerable: true, get: function () {
|
|
|
74
74
|
// ─── Download primitives ─────────────────────────────────────────────────────
|
|
75
75
|
var api_download_1 = require("./lib/api-download");
|
|
76
76
|
Object.defineProperty(exports, "getTrackBuffer", { enumerable: true, get: function () { return api_download_1.getTrackBuffer; } });
|
|
77
|
+
Object.defineProperty(exports, "getTaggedTrack", { enumerable: true, get: function () { return api_download_1.getTaggedTrack; } });
|
|
77
78
|
Object.defineProperty(exports, "downloadTrackToFile", { enumerable: true, get: function () { return api_download_1.downloadTrackToFile; } });
|
|
78
79
|
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
79
80
|
// The same config the CLI uses (env var, global path resolution, arl storage).
|
|
@@ -88,6 +89,13 @@ var gerdur_core_1 = require("gerdur-core");
|
|
|
88
89
|
Object.defineProperty(exports, "initDeezerApi", { enumerable: true, get: function () { return gerdur_core_1.initDeezerApi; } });
|
|
89
90
|
Object.defineProperty(exports, "parseInfo", { enumerable: true, get: function () { return gerdur_core_1.parseInfo; } });
|
|
90
91
|
Object.defineProperty(exports, "searchMusic", { enumerable: true, get: function () { return gerdur_core_1.searchMusic; } });
|
|
92
|
+
Object.defineProperty(exports, "searchPublicApi", { enumerable: true, get: function () { return gerdur_core_1.searchPublicApi; } });
|
|
93
|
+
Object.defineProperty(exports, "searchTracks", { enumerable: true, get: function () { return gerdur_core_1.searchTracks; } });
|
|
94
|
+
Object.defineProperty(exports, "searchAlbums", { enumerable: true, get: function () { return gerdur_core_1.searchAlbums; } });
|
|
95
|
+
Object.defineProperty(exports, "searchArtists", { enumerable: true, get: function () { return gerdur_core_1.searchArtists; } });
|
|
96
|
+
Object.defineProperty(exports, "searchPlaylists", { enumerable: true, get: function () { return gerdur_core_1.searchPlaylists; } });
|
|
97
|
+
Object.defineProperty(exports, "buildAdvancedQuery", { enumerable: true, get: function () { return gerdur_core_1.buildAdvancedQuery; } });
|
|
98
|
+
Object.defineProperty(exports, "suggest", { enumerable: true, get: function () { return gerdur_core_1.suggest; } });
|
|
91
99
|
Object.defineProperty(exports, "getUser", { enumerable: true, get: function () { return gerdur_core_1.getUser; } });
|
|
92
100
|
Object.defineProperty(exports, "getTrackInfo", { enumerable: true, get: function () { return gerdur_core_1.getTrackInfo; } });
|
|
93
101
|
Object.defineProperty(exports, "getAlbumInfo", { enumerable: true, get: function () { return gerdur_core_1.getAlbumInfo; } });
|
|
@@ -98,4 +106,9 @@ Object.defineProperty(exports, "getArtistInfo", { enumerable: true, get: functio
|
|
|
98
106
|
Object.defineProperty(exports, "getDiscography", { enumerable: true, get: function () { return gerdur_core_1.getDiscography; } });
|
|
99
107
|
Object.defineProperty(exports, "getLyrics", { enumerable: true, get: function () { return gerdur_core_1.getLyrics; } });
|
|
100
108
|
Object.defineProperty(exports, "getTrackDownloadUrl", { enumerable: true, get: function () { return gerdur_core_1.getTrackDownloadUrl; } });
|
|
109
|
+
Object.defineProperty(exports, "resolveDownloadUrls", { enumerable: true, get: function () { return gerdur_core_1.resolveDownloadUrls; } });
|
|
110
|
+
Object.defineProperty(exports, "addTrackTags", { enumerable: true, get: function () { return gerdur_core_1.addTrackTags; } });
|
|
111
|
+
Object.defineProperty(exports, "getRichAlbum", { enumerable: true, get: function () { return gerdur_core_1.getRichAlbum; } });
|
|
112
|
+
Object.defineProperty(exports, "normalizeContributors", { enumerable: true, get: function () { return gerdur_core_1.normalizeContributors; } });
|
|
113
|
+
Object.defineProperty(exports, "toLrc", { enumerable: true, get: function () { return gerdur_core_1.toLrc; } });
|
|
101
114
|
Object.defineProperty(exports, "GeoBlocked", { enumerable: true, get: function () { return gerdur_core_1.GeoBlocked; } });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
+
import type { AddTrackTagsOptions } from 'gerdur-core';
|
|
2
3
|
import type { trackType } from 'gerdur-core/types';
|
|
3
4
|
/** Music quality accepted by the API. */
|
|
4
5
|
export type Quality = '128' | '320' | 'flac' | 1 | 3 | 9;
|
|
@@ -10,18 +11,30 @@ export interface GetTrackBufferOptions {
|
|
|
10
11
|
* before giving up. Default true.
|
|
11
12
|
*/
|
|
12
13
|
fallbackQuality?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Extra metadata-writer options passed straight to `gerdur-core.addTrackTags`
|
|
16
|
+
* (e.g. `{embedArtistImage: false, richCredits: false}`). `coverSize` above
|
|
17
|
+
* wins over `metadata.coverSize`.
|
|
18
|
+
*/
|
|
19
|
+
metadata?: AddTrackTagsOptions;
|
|
13
20
|
}
|
|
14
21
|
/**
|
|
15
22
|
* Download, decrypt, and tag a single track entirely in memory.
|
|
16
23
|
*
|
|
17
24
|
* Returns the ready-to-write audio Buffer (MP3 or FLAC), or `null` if the track
|
|
18
25
|
* is not available for download. Nothing is written to disk and nothing is
|
|
19
|
-
* logged.
|
|
26
|
+
* logged. Use `getTaggedTrack` if you also want the structured metadata / LRC.
|
|
20
27
|
*
|
|
21
28
|
* @param track Track info (e.g. from `getTrackInfo` or `parseUrl`).
|
|
22
29
|
* @param quality '128' | '320' | 'flac' (or numeric 1 | 3 | 9).
|
|
23
30
|
*/
|
|
24
|
-
export declare const getTrackBuffer: (track: trackType, quality?: Quality,
|
|
31
|
+
export declare const getTrackBuffer: (track: trackType, quality?: Quality, options?: GetTrackBufferOptions) => Promise<Buffer | null>;
|
|
32
|
+
/**
|
|
33
|
+
* Like `getTrackBuffer`, but returns `{buffer, model}` — `model` carries every
|
|
34
|
+
* field gerdur pulled from Deezer, including `model.lyricsSynced` (an LRC
|
|
35
|
+
* document) when the track has time-synced lyrics.
|
|
36
|
+
*/
|
|
37
|
+
export declare const getTaggedTrack: (track: trackType, quality?: Quality, options?: GetTrackBufferOptions) => Promise<import("gerdur-core").TaggedTrack | null>;
|
|
25
38
|
export interface DownloadTrackOptions extends GetTrackBufferOptions {
|
|
26
39
|
/**
|
|
27
40
|
* Output file path or a `saveLayout` template (e.g. `{ART_NAME} - {SNG_TITLE}`).
|
|
@@ -37,12 +50,16 @@ export interface DownloadTrackOptions extends GetTrackBufferOptions {
|
|
|
37
50
|
trackNumber?: boolean;
|
|
38
51
|
/** Re-download even if the destination file already exists. Default false. */
|
|
39
52
|
overwrite?: boolean;
|
|
53
|
+
/** Write a `.lrc` sidecar next to the audio when the track has synced lyrics. Default true. */
|
|
54
|
+
lrc?: boolean;
|
|
40
55
|
}
|
|
41
56
|
export interface DownloadResult {
|
|
42
57
|
/** The path the file was written to (or would have been, if skipped). */
|
|
43
58
|
path: string;
|
|
44
59
|
/** True if the file was written; false if skipped because it already existed. */
|
|
45
60
|
written: boolean;
|
|
61
|
+
/** Path of the `.lrc` sidecar, if one was written. */
|
|
62
|
+
lrcPath?: string;
|
|
46
63
|
}
|
|
47
64
|
/**
|
|
48
65
|
* Download a single track to disk (silent). Resolves the destination path from
|
|
@@ -3,7 +3,7 @@ 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.downloadTrackToFile = exports.getTrackBuffer = void 0;
|
|
6
|
+
exports.downloadTrackToFile = exports.getTaggedTrack = exports.getTrackBuffer = void 0;
|
|
7
7
|
const fs_1 = require("fs");
|
|
8
8
|
const path_1 = require("path");
|
|
9
9
|
const got_1 = __importDefault(require("got"));
|
|
@@ -22,18 +22,12 @@ const resolveQuality = (quality) => {
|
|
|
22
22
|
const key = String(quality).toLowerCase();
|
|
23
23
|
return QUALITY_MAP[key] ?? QUALITY_MAP['320'];
|
|
24
24
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*
|
|
32
|
-
* @param track Track info (e.g. from `getTrackInfo` or `parseUrl`).
|
|
33
|
-
* @param quality '128' | '320' | 'flac' (or numeric 1 | 3 | 9).
|
|
34
|
-
*/
|
|
35
|
-
const getTrackBuffer = async (track, quality = '320', { coverSize = 500, fallbackQuality = true } = {}) => {
|
|
36
|
-
const order = fallbackQuality ? [quality, '320', '128'] : [quality];
|
|
25
|
+
const tagOptions = (o) => ({
|
|
26
|
+
...o.metadata,
|
|
27
|
+
coverSize: o.coverSize ?? o.metadata?.coverSize ?? 500,
|
|
28
|
+
});
|
|
29
|
+
const fetchDecryptTag = async (track, quality, options) => {
|
|
30
|
+
const order = options.fallbackQuality === false ? [quality] : [quality, '320', '128'];
|
|
37
31
|
const tried = new Set();
|
|
38
32
|
for (const q of order) {
|
|
39
33
|
const { q: qNum } = resolveQuality(q);
|
|
@@ -54,13 +48,34 @@ const getTrackBuffer = async (track, quality = '320', { coverSize = 500, fallbac
|
|
|
54
48
|
if (!trackData) {
|
|
55
49
|
continue;
|
|
56
50
|
}
|
|
57
|
-
const { body } = await (0, got_1.default)(trackData.trackUrl, { responseType: 'buffer' });
|
|
51
|
+
const { body } = await (0, got_1.default)(trackData.trackUrl, { responseType: 'buffer', agent: { http: gerdur_core_1.httpAgent, https: gerdur_core_1.httpsAgent } });
|
|
58
52
|
const decrypted = trackData.isEncrypted ? (0, decrypt_1.decryptDownload)(body, track.SNG_ID) : body;
|
|
59
|
-
return (0, gerdur_core_1.addTrackTags)(decrypted, track,
|
|
53
|
+
return (0, gerdur_core_1.addTrackTags)(decrypted, track, tagOptions(options));
|
|
60
54
|
}
|
|
61
55
|
return null;
|
|
62
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* Download, decrypt, and tag a single track entirely in memory.
|
|
59
|
+
*
|
|
60
|
+
* Returns the ready-to-write audio Buffer (MP3 or FLAC), or `null` if the track
|
|
61
|
+
* is not available for download. Nothing is written to disk and nothing is
|
|
62
|
+
* logged. Use `getTaggedTrack` if you also want the structured metadata / LRC.
|
|
63
|
+
*
|
|
64
|
+
* @param track Track info (e.g. from `getTrackInfo` or `parseUrl`).
|
|
65
|
+
* @param quality '128' | '320' | 'flac' (or numeric 1 | 3 | 9).
|
|
66
|
+
*/
|
|
67
|
+
const getTrackBuffer = async (track, quality = '320', options = {}) => {
|
|
68
|
+
const tagged = await fetchDecryptTag(track, quality, options);
|
|
69
|
+
return tagged ? tagged.buffer : null;
|
|
70
|
+
};
|
|
63
71
|
exports.getTrackBuffer = getTrackBuffer;
|
|
72
|
+
/**
|
|
73
|
+
* Like `getTrackBuffer`, but returns `{buffer, model}` — `model` carries every
|
|
74
|
+
* field gerdur pulled from Deezer, including `model.lyricsSynced` (an LRC
|
|
75
|
+
* document) when the track has time-synced lyrics.
|
|
76
|
+
*/
|
|
77
|
+
const getTaggedTrack = (track, quality = '320', options = {}) => fetchDecryptTag(track, quality, options);
|
|
78
|
+
exports.getTaggedTrack = getTaggedTrack;
|
|
64
79
|
/**
|
|
65
80
|
* Download a single track to disk (silent). Resolves the destination path from
|
|
66
81
|
* the `output` template, skips existing files unless `overwrite` is set, and
|
|
@@ -75,15 +90,20 @@ const downloadTrackToFile = async (track, quality = '320', options = {}) => {
|
|
|
75
90
|
if ((0, fs_1.existsSync)(savePath) && !overwrite) {
|
|
76
91
|
return { path: savePath, written: false };
|
|
77
92
|
}
|
|
78
|
-
const
|
|
79
|
-
if (!
|
|
93
|
+
const tagged = await (0, exports.getTaggedTrack)(track, quality, options);
|
|
94
|
+
if (!tagged) {
|
|
80
95
|
return null;
|
|
81
96
|
}
|
|
82
97
|
const dir = (0, path_1.dirname)(savePath);
|
|
83
98
|
if (dir && !(0, fs_1.existsSync)(dir)) {
|
|
84
99
|
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
85
100
|
}
|
|
86
|
-
(0, fs_1.writeFileSync)(savePath, buffer);
|
|
87
|
-
|
|
101
|
+
(0, fs_1.writeFileSync)(savePath, tagged.buffer);
|
|
102
|
+
let lrcPath;
|
|
103
|
+
if (options.lrc !== false && tagged.model.lyricsSynced) {
|
|
104
|
+
lrcPath = savePath.replace(/\.(mp3|flac)$/i, '.lrc');
|
|
105
|
+
(0, fs_1.writeFileSync)(lrcPath, tagged.model.lyricsSynced);
|
|
106
|
+
}
|
|
107
|
+
return { path: savePath, written: true, lrcPath };
|
|
88
108
|
};
|
|
89
109
|
exports.downloadTrackToFile = downloadTrackToFile;
|
package/dist/src/lib/config.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export declare const globalConfigPath: () => string;
|
|
|
12
12
|
* 3. the global config path (created on first write)
|
|
13
13
|
*/
|
|
14
14
|
export declare const resolveConfigFile: (configFile: string) => string;
|
|
15
|
-
type keysType = 'concurrency' | 'saveLayout' | 'saveLayout.track' | 'saveLayout.album' | 'saveLayout.artist' | 'saveLayout.playlist' | 'playlist.resolveFullPath' | 'trackNumber' | 'fallbackTrack' | 'fallbackQuality' | 'overwrite' | 'coverSize' | 'coverSize.128' | 'coverSize.320' | 'coverSize.flac' | 'cookies.arl' | 'cookies.email' | 'cookies.password';
|
|
15
|
+
type keysType = 'concurrency' | 'saveLayout' | 'saveLayout.track' | 'saveLayout.album' | 'saveLayout.artist' | 'saveLayout.playlist' | 'playlist.resolveFullPath' | 'trackNumber' | 'fallbackTrack' | 'fallbackQuality' | 'overwrite' | 'coverSize' | 'coverSize.128' | 'coverSize.320' | 'coverSize.flac' | 'lyrics.lrcFile' | 'cookies.arl' | 'cookies.email' | 'cookies.password';
|
|
16
16
|
declare class Config {
|
|
17
17
|
userConfigLocation: string | null;
|
|
18
18
|
private configFile;
|
package/dist/src/lib/config.js
CHANGED
|
@@ -62,6 +62,9 @@ const defaultConfig = {
|
|
|
62
62
|
'320': 500,
|
|
63
63
|
flac: 1000,
|
|
64
64
|
},
|
|
65
|
+
lyrics: {
|
|
66
|
+
lrcFile: true,
|
|
67
|
+
},
|
|
65
68
|
cookies: {
|
|
66
69
|
arl: 'c973964816688562722418b5200c1515dffaad15a42643ebf87cc72824a54612ec51c2ad42d566743f9e424c774e98ccae7737770acff59251328e6cd598c7bcac38ca269adf78bfb88ec5bbad6cd800db3c0b88b2af645bb22b99e71de26416',
|
|
67
70
|
},
|
|
@@ -94,6 +97,9 @@ class Config {
|
|
|
94
97
|
if (userConfig.coverSize) {
|
|
95
98
|
userConfig.coverSize = { ...defaultConfig.coverSize, ...userConfig.coverSize };
|
|
96
99
|
}
|
|
100
|
+
if (userConfig.lyrics) {
|
|
101
|
+
userConfig.lyrics = { ...defaultConfig.lyrics, ...userConfig.lyrics };
|
|
102
|
+
}
|
|
97
103
|
if (userConfig.cookies) {
|
|
98
104
|
userConfig.cookies = { ...defaultConfig.cookies, ...userConfig.cookies };
|
|
99
105
|
}
|
|
@@ -19,6 +19,8 @@ interface downloadTrackProps {
|
|
|
19
19
|
isQualityFallback?: boolean;
|
|
20
20
|
overwrite?: boolean;
|
|
21
21
|
message?: string;
|
|
22
|
+
/** Write a `.lrc` sidecar for tracks with time-synced lyrics. Default true. */
|
|
23
|
+
lrc?: boolean;
|
|
22
24
|
/**
|
|
23
25
|
* A download URL already resolved for this track by a batch `resolveDownloadUrls`
|
|
24
26
|
* pass. When set, the per-track `get_url` request and quality-fallback round-trips
|
|
@@ -32,5 +34,5 @@ interface downloadTrackProps {
|
|
|
32
34
|
format: string;
|
|
33
35
|
} | null;
|
|
34
36
|
}
|
|
35
|
-
declare const downloadTrack: ({ track, quality, info, coverSizes, path, totalTracks, trackNumber, fallbackTrack, fallbackQuality, isFallback, isQualityFallback, overwrite, message, prefetched, }: downloadTrackProps) => Promise<string | undefined>;
|
|
37
|
+
declare const downloadTrack: ({ track, quality, info, coverSizes, path, totalTracks, trackNumber, fallbackTrack, fallbackQuality, isFallback, isQualityFallback, overwrite, message, lrc, prefetched, }: downloadTrackProps) => Promise<string | undefined>;
|
|
36
38
|
export default downloadTrack;
|
|
@@ -18,7 +18,7 @@ const pipeline = (0, util_1.promisify)(stream_1.default.pipeline);
|
|
|
18
18
|
const simulate = process.env.SIMULATE;
|
|
19
19
|
/** media-API format string -> gerdur's numeric quality */
|
|
20
20
|
const FORMAT_TO_QUALITY = { FLAC: 9, MP3_320: 3, MP3_256: 3, MP3_128: 1, MP3_64: 1 };
|
|
21
|
-
const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTracks, trackNumber = true, fallbackTrack = true, fallbackQuality = true, isFallback = false, isQualityFallback = false, overwrite = false, message = '', prefetched = null, }) => {
|
|
21
|
+
const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTracks, trackNumber = true, fallbackTrack = true, fallbackQuality = true, isFallback = false, isQualityFallback = false, overwrite = false, message = '', lrc = true, prefetched = null, }) => {
|
|
22
22
|
(0, log_update_1.default)(signale_1.default.pending(track.SNG_TITLE + ' by ' + track.ART_NAME + ' from ' + track.ALB_TITLE));
|
|
23
23
|
try {
|
|
24
24
|
let ext = '.mp3', fileSize = 0, downloaded = 0, coverSize = 500;
|
|
@@ -89,6 +89,7 @@ const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTrac
|
|
|
89
89
|
isFallback: true,
|
|
90
90
|
overwrite,
|
|
91
91
|
message,
|
|
92
|
+
lrc,
|
|
92
93
|
});
|
|
93
94
|
}
|
|
94
95
|
else if (fallbackQuality && quality !== 1) {
|
|
@@ -105,6 +106,7 @@ const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTrac
|
|
|
105
106
|
isQualityFallback: true,
|
|
106
107
|
overwrite,
|
|
107
108
|
message,
|
|
109
|
+
lrc,
|
|
108
110
|
});
|
|
109
111
|
}
|
|
110
112
|
(0, log_update_1.default)(signale_1.default.warn(`Skipped "${track.SNG_TITLE}", track not available.`));
|
|
@@ -149,7 +151,7 @@ const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTrac
|
|
|
149
151
|
outFile = (0, fs_1.readFileSync)(tmpfile);
|
|
150
152
|
}
|
|
151
153
|
(0, log_update_1.default)(signale_1.default.pending('Tagging ' + track.SNG_TITLE + ' by ' + track.ART_NAME));
|
|
152
|
-
const trackWithMetadata = await (0, gerdur_core_1.addTrackTags)(outFile, track, coverSize);
|
|
154
|
+
const { buffer: trackWithMetadata, model } = await (0, gerdur_core_1.addTrackTags)(outFile, track, { coverSize });
|
|
153
155
|
// Delete temporary file now
|
|
154
156
|
(0, fs_1.unlinkSync)(tmpfile);
|
|
155
157
|
(0, log_update_1.default)(signale_1.default.pending('Saving ' + track.SNG_TITLE + ' by ' + track.ART_NAME));
|
|
@@ -161,6 +163,9 @@ const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTrac
|
|
|
161
163
|
}
|
|
162
164
|
// Save file to disk
|
|
163
165
|
(0, fs_1.writeFileSync)(savePath, trackWithMetadata);
|
|
166
|
+
if (lrc !== false && model.lyricsSynced) {
|
|
167
|
+
(0, fs_1.writeFileSync)(savePath.replace(/\.(mp3|flac)$/i, '.lrc'), model.lyricsSynced);
|
|
168
|
+
}
|
|
164
169
|
}
|
|
165
170
|
// Print sucess info
|
|
166
171
|
(0, log_update_1.default)(signale_1.default.success(`${isFallback ? chalk_1.default.yellow('[Fallback] ') : ''}${track.SNG_TITLE} by ${track.ART_NAME}`));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { parseInfo, searchMusic } from 'gerdur-core';
|
|
3
3
|
import type { Quality, DownloadTrackOptions, DownloadResult } from './api-download';
|
|
4
|
-
import type { trackType, userType } from 'gerdur-core/types';
|
|
4
|
+
import type { trackType, userType, advancedSearchFilters, publicApiSearchOptions, publicApiSearchResponse, searchResultTrack, suggestResult } from 'gerdur-core/types';
|
|
5
5
|
/** Search result categories accepted by {@link Session.search}. */
|
|
6
6
|
export type SearchType = 'ALBUM' | 'ARTIST' | 'TRACK' | 'PLAYLIST' | 'RADIO' | 'SHOW' | 'USER' | 'LIVESTREAM' | 'CHANNEL';
|
|
7
7
|
export interface SessionOptions {
|
|
@@ -52,6 +52,17 @@ export declare class Session {
|
|
|
52
52
|
parseUrl(url: string): ReturnType<typeof parseInfo>;
|
|
53
53
|
/** Search Deezer. Defaults to track results. */
|
|
54
54
|
search(query: string, types?: SearchType[], limit?: number): ReturnType<typeof searchMusic>;
|
|
55
|
+
/**
|
|
56
|
+
* Search the public REST API with structured filters (`{artist, album, track,
|
|
57
|
+
* label, durMin, durMax, bpmMin, bpmMax}`) plus a free-text `query`. Returns
|
|
58
|
+
* public-API track objects (with `isrc`, `preview`); to download a result,
|
|
59
|
+
* fetch its gw track first with the re-exported `getTrackInfo(id)`.
|
|
60
|
+
*
|
|
61
|
+
* Deezer applies the operators loosely — see `buildAdvancedQuery`.
|
|
62
|
+
*/
|
|
63
|
+
searchAdvanced(filters: advancedSearchFilters, options?: Omit<publicApiSearchOptions, 'type'>): Promise<publicApiSearchResponse<searchResultTrack>>;
|
|
64
|
+
/** `deezer.suggest` autocomplete — for "as you type" UIs. `nb` caps items per type. */
|
|
65
|
+
suggest(query: string, nb?: number): Promise<suggestResult>;
|
|
55
66
|
/** Return a fully tagged audio Buffer for a track (nothing written to disk). */
|
|
56
67
|
getTrackBuffer(track: trackType, quality?: Quality, options?: {}): Promise<Buffer | null>;
|
|
57
68
|
/** Download a single track to disk. */
|
package/dist/src/lib/session.js
CHANGED
|
@@ -57,6 +57,21 @@ class Session {
|
|
|
57
57
|
search(query, types = ['TRACK'], limit) {
|
|
58
58
|
return (0, gerdur_core_1.searchMusic)(query, types, limit);
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Search the public REST API with structured filters (`{artist, album, track,
|
|
62
|
+
* label, durMin, durMax, bpmMin, bpmMax}`) plus a free-text `query`. Returns
|
|
63
|
+
* public-API track objects (with `isrc`, `preview`); to download a result,
|
|
64
|
+
* fetch its gw track first with the re-exported `getTrackInfo(id)`.
|
|
65
|
+
*
|
|
66
|
+
* Deezer applies the operators loosely — see `buildAdvancedQuery`.
|
|
67
|
+
*/
|
|
68
|
+
searchAdvanced(filters, options = {}) {
|
|
69
|
+
return (0, gerdur_core_1.searchTracks)((0, gerdur_core_1.buildAdvancedQuery)(filters), options);
|
|
70
|
+
}
|
|
71
|
+
/** `deezer.suggest` autocomplete — for "as you type" UIs. `nb` caps items per type. */
|
|
72
|
+
suggest(query, nb) {
|
|
73
|
+
return (0, gerdur_core_1.suggest)(query, nb);
|
|
74
|
+
}
|
|
60
75
|
/** Return a fully tagged audio Buffer for a track (nothing written to disk). */
|
|
61
76
|
getTrackBuffer(track, quality = '320', options = {}) {
|
|
62
77
|
return (0, api_download_1.getTrackBuffer)(track, quality, options);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gerdur",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "The crucial streaming module for dabox systems.",
|
|
5
5
|
"author": "Christian",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"adm-zip": "^0.5.16",
|
|
49
49
|
"chalk": "^4.1.2",
|
|
50
50
|
"commander": "^9.5.0",
|
|
51
|
-
"gerdur-core": "^1.0
|
|
51
|
+
"gerdur-core": "^2.1.0",
|
|
52
52
|
"dot-prop": "^6.0.1",
|
|
53
53
|
"got": "^11.8.6",
|
|
54
54
|
"gradient-string": "^2.0.2",
|