gerdur 1.0.1 → 2.0.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 +24 -0
- package/dist/package.json +2 -2
- package/dist/src/gerdur.js +29 -0
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.js +7 -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 +15 -1
- package/dist/src/lib/download-track.js +31 -8
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.0 - 2026-08-31
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Metadata is now much richer** (`gerdur-core@^2.0.0`). Every downloaded file
|
|
8
|
+
gets ReplayGain (`REPLAYGAIN_TRACK_GAIN`), BPM, real `©`/`℗` lines, the true
|
|
9
|
+
original release date (distinct from a reissue date), full studio credits
|
|
10
|
+
(featured artists, mastering / mixing / recording engineers, producers),
|
|
11
|
+
compilation/live flags, iTunes advisory, Deezer ids, and the artist photo as a
|
|
12
|
+
second embedded image. Album/playlist tracks are hydrated with one coalesced
|
|
13
|
+
`song.getData` so the credits are complete.
|
|
14
|
+
- **`.lrc` sidecar files** are written next to the audio for tracks with
|
|
15
|
+
time-synced lyrics. Toggle with `"lyrics": {"lrcFile": false}` in
|
|
16
|
+
`gerdur.config.json`.
|
|
17
|
+
- Cover-art requests are capped at Deezer's real ceiling of 1800 px.
|
|
18
|
+
|
|
19
|
+
### Breaking (programmatic API)
|
|
20
|
+
|
|
21
|
+
- `gerdur-core.addTrackTags` changed signature/return — see its 2.0.0 changelog.
|
|
22
|
+
`getTrackBuffer` is unaffected (still resolves to `Buffer | null`); new
|
|
23
|
+
`getTaggedTrack(track, quality, options)` returns `{buffer, model}` with the
|
|
24
|
+
structured metadata and LRC. `downloadTrackToFile` now also returns `lrcPath`
|
|
25
|
+
when a sidecar was written.
|
|
26
|
+
|
|
3
27
|
## 1.0.1 - 2026-08-30
|
|
4
28
|
|
|
5
29
|
### Changed
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gerdur",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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": "^
|
|
51
|
+
"gerdur-core": "^2.0.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,10 +215,37 @@ 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 = [];
|
|
221
222
|
let m3u8 = [];
|
|
223
|
+
// One batched get_url for the whole selection instead of a request per track.
|
|
224
|
+
// Deezer returns the best licensed format, so pass an ordered preference list.
|
|
225
|
+
const QUALITY_PREF = {
|
|
226
|
+
'1': [1],
|
|
227
|
+
'128': [1],
|
|
228
|
+
'3': [3, 1],
|
|
229
|
+
'320': [3, 1],
|
|
230
|
+
'9': [9, 3, 1],
|
|
231
|
+
flac: [9, 3, 1],
|
|
232
|
+
};
|
|
233
|
+
const prefetchedUrls = new Map();
|
|
234
|
+
if (data.tracks.length > 1 && !process.env.SIMULATE) {
|
|
235
|
+
try {
|
|
236
|
+
const base = QUALITY_PREF[String(options.quality).toLowerCase()] ?? [3, 1];
|
|
237
|
+
const pref = fallbackQuality ? base : [base[0]];
|
|
238
|
+
const resolved = await (0, gerdur_core_1.resolveDownloadUrls)(data.tracks, pref);
|
|
239
|
+
resolved.forEach((r, i) => {
|
|
240
|
+
if (r) {
|
|
241
|
+
prefetchedUrls.set(data.tracks[i].SNG_ID, r);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// batch resolve failed wholesale — every track falls back to per-track resolution
|
|
247
|
+
}
|
|
248
|
+
}
|
|
222
249
|
await queue.addAll(data.tracks.map((track, index) => {
|
|
223
250
|
return async () => {
|
|
224
251
|
const savedPath = await (0, download_track_1.default)({
|
|
@@ -233,6 +260,8 @@ const startDownload = async (saveLayout, url, skipPrompt) => {
|
|
|
233
260
|
fallbackQuality,
|
|
234
261
|
overwrite,
|
|
235
262
|
message: `(${index}/${data.tracks.length})`,
|
|
263
|
+
lrc,
|
|
264
|
+
prefetched: prefetchedUrls.get(track.SNG_ID) ?? null,
|
|
236
265
|
});
|
|
237
266
|
// Add to saved list
|
|
238
267
|
if (savedPath) {
|
package/dist/src/index.d.ts
CHANGED
|
@@ -56,7 +56,8 @@ 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, 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';
|
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.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).
|
|
@@ -98,4 +99,9 @@ Object.defineProperty(exports, "getArtistInfo", { enumerable: true, get: functio
|
|
|
98
99
|
Object.defineProperty(exports, "getDiscography", { enumerable: true, get: function () { return gerdur_core_1.getDiscography; } });
|
|
99
100
|
Object.defineProperty(exports, "getLyrics", { enumerable: true, get: function () { return gerdur_core_1.getLyrics; } });
|
|
100
101
|
Object.defineProperty(exports, "getTrackDownloadUrl", { enumerable: true, get: function () { return gerdur_core_1.getTrackDownloadUrl; } });
|
|
102
|
+
Object.defineProperty(exports, "resolveDownloadUrls", { enumerable: true, get: function () { return gerdur_core_1.resolveDownloadUrls; } });
|
|
103
|
+
Object.defineProperty(exports, "addTrackTags", { enumerable: true, get: function () { return gerdur_core_1.addTrackTags; } });
|
|
104
|
+
Object.defineProperty(exports, "getRichAlbum", { enumerable: true, get: function () { return gerdur_core_1.getRichAlbum; } });
|
|
105
|
+
Object.defineProperty(exports, "normalizeContributors", { enumerable: true, get: function () { return gerdur_core_1.normalizeContributors; } });
|
|
106
|
+
Object.defineProperty(exports, "toLrc", { enumerable: true, get: function () { return gerdur_core_1.toLrc; } });
|
|
101
107
|
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,20 @@ 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;
|
|
24
|
+
/**
|
|
25
|
+
* A download URL already resolved for this track by a batch `resolveDownloadUrls`
|
|
26
|
+
* pass. When set, the per-track `get_url` request and quality-fallback round-trips
|
|
27
|
+
* are skipped and `quality` is taken from the format Deezer actually granted.
|
|
28
|
+
* Falls through to the normal path when absent or `null` (track not in the batch).
|
|
29
|
+
*/
|
|
30
|
+
prefetched?: {
|
|
31
|
+
trackUrl: string;
|
|
32
|
+
isEncrypted: boolean;
|
|
33
|
+
fileSize: number;
|
|
34
|
+
format: string;
|
|
35
|
+
} | null;
|
|
22
36
|
}
|
|
23
|
-
declare const downloadTrack: ({ track, quality, info, coverSizes, path, totalTracks, trackNumber, fallbackTrack, fallbackQuality, isFallback, isQualityFallback, overwrite, message, }: 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>;
|
|
24
38
|
export default downloadTrack;
|
|
@@ -16,10 +16,17 @@ const util_2 = require("./util");
|
|
|
16
16
|
const decrypt_1 = require("./decrypt");
|
|
17
17
|
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
|
+
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 = '', lrc = true, prefetched = null, }) => {
|
|
20
22
|
(0, log_update_1.default)(signale_1.default.pending(track.SNG_TITLE + ' by ' + track.ART_NAME + ' from ' + track.ALB_TITLE));
|
|
21
23
|
try {
|
|
22
24
|
let ext = '.mp3', fileSize = 0, downloaded = 0, coverSize = 500;
|
|
25
|
+
// A batch resolve already picked the best licensed format for this track —
|
|
26
|
+
// align `quality` with it so the extension, cover size and file size match.
|
|
27
|
+
if (prefetched && FORMAT_TO_QUALITY[prefetched.format]) {
|
|
28
|
+
quality = FORMAT_TO_QUALITY[prefetched.format];
|
|
29
|
+
}
|
|
23
30
|
switch (quality) {
|
|
24
31
|
case 1:
|
|
25
32
|
case '1':
|
|
@@ -54,12 +61,17 @@ const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTrac
|
|
|
54
61
|
return savePath;
|
|
55
62
|
}
|
|
56
63
|
let trackData;
|
|
57
|
-
|
|
58
|
-
trackData =
|
|
64
|
+
if (prefetched) {
|
|
65
|
+
trackData = prefetched;
|
|
59
66
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
else {
|
|
68
|
+
try {
|
|
69
|
+
trackData = await (0, gerdur_core_1.getTrackDownloadUrl)(track, quality);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (!(err instanceof gerdur_core_1.GeoBlocked) || !track.FALLBACK) {
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
63
75
|
}
|
|
64
76
|
}
|
|
65
77
|
if (!trackData) {
|
|
@@ -77,6 +89,7 @@ const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTrac
|
|
|
77
89
|
isFallback: true,
|
|
78
90
|
overwrite,
|
|
79
91
|
message,
|
|
92
|
+
lrc,
|
|
80
93
|
});
|
|
81
94
|
}
|
|
82
95
|
else if (fallbackQuality && quality !== 1) {
|
|
@@ -93,6 +106,7 @@ const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTrac
|
|
|
93
106
|
isQualityFallback: true,
|
|
94
107
|
overwrite,
|
|
95
108
|
message,
|
|
109
|
+
lrc,
|
|
96
110
|
});
|
|
97
111
|
}
|
|
98
112
|
(0, log_update_1.default)(signale_1.default.warn(`Skipped "${track.SNG_TITLE}", track not available.`));
|
|
@@ -114,7 +128,13 @@ const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTrac
|
|
|
114
128
|
const bar = (0, util_2.progressBar)(fileSize, 40);
|
|
115
129
|
const humanSizeTotal = (fileSize / 1024 / 1024).toFixed(2);
|
|
116
130
|
let transferredLast = downloaded;
|
|
117
|
-
await pipeline(got_1.default
|
|
131
|
+
await pipeline(got_1.default
|
|
132
|
+
.stream(trackData.trackUrl, {
|
|
133
|
+
responseType: 'buffer',
|
|
134
|
+
headers,
|
|
135
|
+
agent: { http: gerdur_core_1.httpAgent, https: gerdur_core_1.httpsAgent },
|
|
136
|
+
})
|
|
137
|
+
.on('downloadProgress', ({ transferred }) => {
|
|
118
138
|
// Report download progress
|
|
119
139
|
transferred += downloaded;
|
|
120
140
|
if (transferred - transferredLast > 50000) {
|
|
@@ -131,7 +151,7 @@ const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTrac
|
|
|
131
151
|
outFile = (0, fs_1.readFileSync)(tmpfile);
|
|
132
152
|
}
|
|
133
153
|
(0, log_update_1.default)(signale_1.default.pending('Tagging ' + track.SNG_TITLE + ' by ' + track.ART_NAME));
|
|
134
|
-
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 });
|
|
135
155
|
// Delete temporary file now
|
|
136
156
|
(0, fs_1.unlinkSync)(tmpfile);
|
|
137
157
|
(0, log_update_1.default)(signale_1.default.pending('Saving ' + track.SNG_TITLE + ' by ' + track.ART_NAME));
|
|
@@ -143,6 +163,9 @@ const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTrac
|
|
|
143
163
|
}
|
|
144
164
|
// Save file to disk
|
|
145
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
|
+
}
|
|
146
169
|
}
|
|
147
170
|
// Print sucess info
|
|
148
171
|
(0, log_update_1.default)(signale_1.default.success(`${isFallback ? chalk_1.default.yellow('[Fallback] ') : ''}${track.SNG_TITLE} by ${track.ART_NAME}`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gerdur",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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": "^
|
|
51
|
+
"gerdur-core": "^2.0.0",
|
|
52
52
|
"dot-prop": "^6.0.1",
|
|
53
53
|
"got": "^11.8.6",
|
|
54
54
|
"gradient-string": "^2.0.2",
|