gerdur-core 1.0.3 → 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 +63 -0
- package/README.md +17 -9
- package/dist/api/request.js +66 -45
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -1
- package/dist/lib/get-url.d.ts +21 -0
- package/dist/lib/get-url.js +91 -46
- package/dist/lib/http.d.ts +5 -1
- package/dist/lib/http.js +24 -4
- 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/tracks.d.ts +13 -12
- package/package.json +1 -1
|
@@ -1,9 +1,63 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
import type {
|
|
2
|
+
import type { TrackTagModel } from './model';
|
|
3
|
+
import type { RichAlbum } from './rich-album';
|
|
4
|
+
import type { lyricsType, trackType, trackTypePublicApi } from '../types';
|
|
5
|
+
export { normalizeContributors } from './contributors';
|
|
6
|
+
export type { NormalizedContributors } from './contributors';
|
|
7
|
+
export { toLrc } from './lrc';
|
|
8
|
+
export { getRichAlbum } from './rich-album';
|
|
9
|
+
export type { RichAlbum } from './rich-album';
|
|
10
|
+
export { buildTagModel } from './model';
|
|
11
|
+
export type { TrackTagModel, Person } from './model';
|
|
12
|
+
export { downloadAlbumCover, downloadArtistImage, MAX_COVER_SIZE } from './abumCover';
|
|
13
|
+
export interface AddTrackTagsOptions {
|
|
14
|
+
/** embedded cover size in px, 56–1800 (default 1000) */
|
|
15
|
+
coverSize?: number;
|
|
16
|
+
/** pre-fetched album cover; skips the download when provided (`null` = none) */
|
|
17
|
+
cover?: Buffer | null;
|
|
18
|
+
/** pre-fetched artist image */
|
|
19
|
+
artistImage?: Buffer | null;
|
|
20
|
+
/** pre-fetched merged album info; skips `getRichAlbum` (fetch once per album, pass here) */
|
|
21
|
+
album?: RichAlbum | null;
|
|
22
|
+
/** pre-fetched lyrics (`song.getLyrics` shape); skips the lyrics fetch */
|
|
23
|
+
lyrics?: lyricsType | null;
|
|
24
|
+
/** pre-fetched public `/track/` payload (BPM source) */
|
|
25
|
+
publicTrack?: trackTypePublicApi | null;
|
|
26
|
+
/** embed front cover art. default true */
|
|
27
|
+
embedCover?: boolean;
|
|
28
|
+
/** embed the artist photo as a second picture. default true */
|
|
29
|
+
embedArtistImage?: boolean;
|
|
30
|
+
/** fetch + embed lyrics. default true */
|
|
31
|
+
writeLyrics?: boolean;
|
|
32
|
+
/** embed synced LRC (FLAC Vorbis comment only; MP3 has no v2.3 synced frame). default true */
|
|
33
|
+
embedSyncedLyrics?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* hydrate `SNG_CONTRIBUTORS` / `VERSION` / `GAIN` from `song.getData` when the
|
|
36
|
+
* track came from an album/playlist listing (which omits them), and pull BPM
|
|
37
|
+
* from the public API. default true.
|
|
38
|
+
*/
|
|
39
|
+
richCredits?: boolean;
|
|
40
|
+
/** write `DEEZER_*_ID`, url slug, provider/label ids. default true */
|
|
41
|
+
deezerIds?: boolean;
|
|
42
|
+
/** write popularity rank. default true */
|
|
43
|
+
includeRank?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface TaggedTrack {
|
|
46
|
+
/** the audio with tags written */
|
|
47
|
+
buffer: Buffer;
|
|
48
|
+
/** everything gerdur pulled together for this track — use `model.lyricsSynced` for a `.lrc` sidecar */
|
|
49
|
+
model: TrackTagModel;
|
|
50
|
+
}
|
|
3
51
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
52
|
+
* Pull together everything Deezer has for a track and write it into the audio.
|
|
53
|
+
*
|
|
54
|
+
* Sniffs `fLaC` vs MP3 and dispatches to the FLAC / ID3 writer. Fetches album
|
|
55
|
+
* info, lyrics, cover and (for album/playlist tracks) full credits + BPM in
|
|
56
|
+
* parallel — every call is memoised and in-flight-coalesced, so tagging all
|
|
57
|
+
* tracks of one album hits each metadata endpoint once. Pass any of
|
|
58
|
+
* `options.{album,lyrics,cover,publicTrack}` to skip the corresponding fetch.
|
|
59
|
+
*
|
|
60
|
+
* @returns `{buffer, model}` — `model` carries the structured metadata and,
|
|
61
|
+
* when available, `model.lyricsSynced` (an LRC document for a sidecar file).
|
|
8
62
|
*/
|
|
9
|
-
export declare const addTrackTags: (trackBuffer: Buffer,
|
|
63
|
+
export declare const addTrackTags: (trackBuffer: Buffer, trackInput: trackType, options?: AddTrackTagsOptions) => Promise<TaggedTrack>;
|
|
@@ -1,44 +1,115 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.addTrackTags = void 0;
|
|
3
|
+
exports.addTrackTags = exports.MAX_COVER_SIZE = exports.downloadArtistImage = exports.downloadAlbumCover = exports.buildTagModel = exports.getRichAlbum = exports.toLrc = exports.normalizeContributors = void 0;
|
|
4
4
|
const abumCover_1 = require("./abumCover");
|
|
5
5
|
const getTrackLyrics_1 = require("./getTrackLyrics");
|
|
6
6
|
const id3_1 = require("./id3");
|
|
7
7
|
const flacmetata_1 = require("./flacmetata");
|
|
8
|
+
const rich_album_1 = require("./rich-album");
|
|
9
|
+
const model_1 = require("./model");
|
|
8
10
|
const api_1 = require("../api");
|
|
9
|
-
|
|
11
|
+
var contributors_1 = require("./contributors");
|
|
12
|
+
Object.defineProperty(exports, "normalizeContributors", { enumerable: true, get: function () { return contributors_1.normalizeContributors; } });
|
|
13
|
+
var lrc_1 = require("./lrc");
|
|
14
|
+
Object.defineProperty(exports, "toLrc", { enumerable: true, get: function () { return lrc_1.toLrc; } });
|
|
15
|
+
var rich_album_2 = require("./rich-album");
|
|
16
|
+
Object.defineProperty(exports, "getRichAlbum", { enumerable: true, get: function () { return rich_album_2.getRichAlbum; } });
|
|
17
|
+
var model_2 = require("./model");
|
|
18
|
+
Object.defineProperty(exports, "buildTagModel", { enumerable: true, get: function () { return model_2.buildTagModel; } });
|
|
19
|
+
var abumCover_2 = require("./abumCover");
|
|
20
|
+
Object.defineProperty(exports, "downloadAlbumCover", { enumerable: true, get: function () { return abumCover_2.downloadAlbumCover; } });
|
|
21
|
+
Object.defineProperty(exports, "downloadArtistImage", { enumerable: true, get: function () { return abumCover_2.downloadArtistImage; } });
|
|
22
|
+
Object.defineProperty(exports, "MAX_COVER_SIZE", { enumerable: true, get: function () { return abumCover_2.MAX_COVER_SIZE; } });
|
|
23
|
+
const DEFAULTS = {
|
|
24
|
+
coverSize: 1000,
|
|
25
|
+
embedCover: true,
|
|
26
|
+
embedArtistImage: true,
|
|
27
|
+
writeLyrics: true,
|
|
28
|
+
embedSyncedLyrics: true,
|
|
29
|
+
richCredits: true,
|
|
30
|
+
deezerIds: true,
|
|
31
|
+
includeRank: true,
|
|
32
|
+
};
|
|
33
|
+
const hydrate = async (track) => {
|
|
34
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
35
|
+
if (track.SNG_CONTRIBUTORS !== undefined && track.VERSION !== undefined && track.GAIN !== undefined) {
|
|
36
|
+
return track;
|
|
37
|
+
}
|
|
10
38
|
try {
|
|
11
|
-
|
|
39
|
+
const full = await (0, api_1.getTrackInfo)(track.SNG_ID);
|
|
40
|
+
return {
|
|
41
|
+
...full,
|
|
42
|
+
...track,
|
|
43
|
+
SNG_CONTRIBUTORS: (_a = full.SNG_CONTRIBUTORS) !== null && _a !== void 0 ? _a : track.SNG_CONTRIBUTORS,
|
|
44
|
+
VERSION: (_b = full.VERSION) !== null && _b !== void 0 ? _b : track.VERSION,
|
|
45
|
+
GAIN: (_c = track.GAIN) !== null && _c !== void 0 ? _c : full.GAIN,
|
|
46
|
+
RANK: (_d = track.RANK) !== null && _d !== void 0 ? _d : full.RANK,
|
|
47
|
+
URL_REWRITING: (_e = track.URL_REWRITING) !== null && _e !== void 0 ? _e : full.URL_REWRITING,
|
|
48
|
+
PROVIDER_ID: (_f = track.PROVIDER_ID) !== null && _f !== void 0 ? _f : full.PROVIDER_ID,
|
|
49
|
+
EXPLICIT_TRACK_CONTENT: (_g = track.EXPLICIT_TRACK_CONTENT) !== null && _g !== void 0 ? _g : full.EXPLICIT_TRACK_CONTENT,
|
|
50
|
+
ART_PICTURE: track.ART_PICTURE || full.ART_PICTURE,
|
|
51
|
+
};
|
|
12
52
|
}
|
|
13
|
-
catch
|
|
14
|
-
return
|
|
53
|
+
catch {
|
|
54
|
+
return track;
|
|
15
55
|
}
|
|
16
56
|
};
|
|
17
57
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
58
|
+
* Pull together everything Deezer has for a track and write it into the audio.
|
|
59
|
+
*
|
|
60
|
+
* Sniffs `fLaC` vs MP3 and dispatches to the FLAC / ID3 writer. Fetches album
|
|
61
|
+
* info, lyrics, cover and (for album/playlist tracks) full credits + BPM in
|
|
62
|
+
* parallel — every call is memoised and in-flight-coalesced, so tagging all
|
|
63
|
+
* tracks of one album hits each metadata endpoint once. Pass any of
|
|
64
|
+
* `options.{album,lyrics,cover,publicTrack}` to skip the corresponding fetch.
|
|
65
|
+
*
|
|
66
|
+
* @returns `{buffer, model}` — `model` carries the structured metadata and,
|
|
67
|
+
* when available, `model.lyricsSynced` (an LRC document for a sidecar file).
|
|
22
68
|
*/
|
|
23
|
-
const addTrackTags = async (trackBuffer,
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
69
|
+
const addTrackTags = async (trackBuffer, trackInput, options = {}) => {
|
|
70
|
+
const opt = { ...DEFAULTS, ...options };
|
|
71
|
+
const track = opt.richCredits ? await hydrate(trackInput) : trackInput;
|
|
72
|
+
const [album, lyrics, publicTrack, cover, artistImage] = await Promise.all([
|
|
73
|
+
options.album !== undefined ? Promise.resolve(options.album) : (0, rich_album_1.getRichAlbum)(track.ALB_ID).catch(() => null),
|
|
74
|
+
options.lyrics !== undefined
|
|
75
|
+
? Promise.resolve(options.lyrics)
|
|
76
|
+
: opt.writeLyrics
|
|
77
|
+
? (0, getTrackLyrics_1.getTrackLyrics)(track).catch(() => null)
|
|
78
|
+
: Promise.resolve(null),
|
|
79
|
+
options.publicTrack !== undefined
|
|
80
|
+
? Promise.resolve(options.publicTrack)
|
|
81
|
+
: opt.richCredits
|
|
82
|
+
? (0, api_1.getTrackInfoPublicApi)(track.SNG_ID).catch(() => null)
|
|
83
|
+
: Promise.resolve(null),
|
|
84
|
+
options.cover !== undefined
|
|
85
|
+
? Promise.resolve(options.cover)
|
|
86
|
+
: opt.embedCover
|
|
87
|
+
? (0, abumCover_1.downloadAlbumCover)(track, opt.coverSize)
|
|
88
|
+
: Promise.resolve(null),
|
|
89
|
+
options.artistImage !== undefined
|
|
90
|
+
? Promise.resolve(options.artistImage)
|
|
91
|
+
: opt.embedArtistImage
|
|
92
|
+
? (0, abumCover_1.downloadArtistImage)(track).catch(() => null)
|
|
93
|
+
: Promise.resolve(null),
|
|
28
94
|
]);
|
|
29
|
-
if (
|
|
30
|
-
track.LYRICS = lyrics;
|
|
31
|
-
}
|
|
32
|
-
if (track.ART_NAME.toLowerCase() === 'various') {
|
|
95
|
+
if (track.ART_NAME && track.ART_NAME.toLowerCase() === 'various') {
|
|
33
96
|
track.ART_NAME = 'Various Artists';
|
|
34
97
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
98
|
+
const model = (0, model_1.buildTagModel)({
|
|
99
|
+
track,
|
|
100
|
+
album: album || undefined,
|
|
101
|
+
publicTrack,
|
|
102
|
+
lyrics,
|
|
103
|
+
cover: opt.embedCover ? cover : null,
|
|
104
|
+
artistImage: opt.embedArtistImage ? artistImage : null,
|
|
105
|
+
coverSize: opt.coverSize,
|
|
106
|
+
deezerIds: opt.deezerIds,
|
|
107
|
+
includeRank: opt.includeRank,
|
|
108
|
+
});
|
|
39
109
|
const isFlac = trackBuffer.slice(0, 4).toString('ascii') === 'fLaC';
|
|
40
|
-
|
|
41
|
-
? (0, flacmetata_1.writeMetadataFlac)(trackBuffer,
|
|
42
|
-
: (0, id3_1.writeMetadataMp3)(trackBuffer,
|
|
110
|
+
const buffer = isFlac
|
|
111
|
+
? (0, flacmetata_1.writeMetadataFlac)(trackBuffer, model, { embedSyncedLyrics: opt.embedSyncedLyrics })
|
|
112
|
+
: (0, id3_1.writeMetadataMp3)(trackBuffer, model);
|
|
113
|
+
return { buffer, model };
|
|
43
114
|
};
|
|
44
115
|
exports.addTrackTags = addTrackTags;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { lyricsSync } from '../types';
|
|
2
|
+
export interface LrcMeta {
|
|
3
|
+
title?: string;
|
|
4
|
+
artist?: string;
|
|
5
|
+
album?: string;
|
|
6
|
+
writers?: string;
|
|
7
|
+
length?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Render Deezer's `LYRICS_SYNC_JSON` as a standard LRC document.
|
|
11
|
+
*
|
|
12
|
+
* Deezer already gives each line an `lrc_timestamp` like `[00:52.64]`; we trust
|
|
13
|
+
* `milliseconds` and re-stamp so the output is well-formed even when a line has
|
|
14
|
+
* an empty timestamp (section breaks come through as `{line: ""}`).
|
|
15
|
+
*/
|
|
16
|
+
export declare const toLrc: (sync: lyricsSync[] | undefined, meta?: LrcMeta) => string | null;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toLrc = void 0;
|
|
4
|
+
const stamp = (ms) => {
|
|
5
|
+
const cs = Math.round(ms / 10);
|
|
6
|
+
const m = Math.floor(cs / 6000);
|
|
7
|
+
const s = Math.floor((cs % 6000) / 100);
|
|
8
|
+
const c = cs % 100;
|
|
9
|
+
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}.${String(c).padStart(2, '0')}`;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Render Deezer's `LYRICS_SYNC_JSON` as a standard LRC document.
|
|
13
|
+
*
|
|
14
|
+
* Deezer already gives each line an `lrc_timestamp` like `[00:52.64]`; we trust
|
|
15
|
+
* `milliseconds` and re-stamp so the output is well-formed even when a line has
|
|
16
|
+
* an empty timestamp (section breaks come through as `{line: ""}`).
|
|
17
|
+
*/
|
|
18
|
+
const toLrc = (sync, meta = {}) => {
|
|
19
|
+
if (!sync || !sync.length) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const head = [];
|
|
23
|
+
if (meta.artist)
|
|
24
|
+
head.push(`[ar:${meta.artist}]`);
|
|
25
|
+
if (meta.title)
|
|
26
|
+
head.push(`[ti:${meta.title}]`);
|
|
27
|
+
if (meta.album)
|
|
28
|
+
head.push(`[al:${meta.album}]`);
|
|
29
|
+
if (meta.writers)
|
|
30
|
+
head.push(`[au:${meta.writers}]`);
|
|
31
|
+
if (meta.length)
|
|
32
|
+
head.push(`[length:${stamp(meta.length * 1000)}]`);
|
|
33
|
+
head.push('[re:gerdur]');
|
|
34
|
+
const lines = sync
|
|
35
|
+
.filter((l) => l.line && l.line.trim().length)
|
|
36
|
+
.map((l) => {
|
|
37
|
+
const ms = Number(l.milliseconds);
|
|
38
|
+
const ts = l.milliseconds && Number.isFinite(ms) ? `[${stamp(ms)}]` : l.lrc_timestamp || '';
|
|
39
|
+
return `${ts}${l.line}`;
|
|
40
|
+
});
|
|
41
|
+
return [...head, ...lines].join('\n') + '\n';
|
|
42
|
+
};
|
|
43
|
+
exports.toLrc = toLrc;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import type { RichAlbum } from './rich-album';
|
|
3
|
+
import type { lyricsType, trackType, trackTypePublicApi } from '../types';
|
|
4
|
+
export interface TagModelInput {
|
|
5
|
+
track: trackType;
|
|
6
|
+
album?: RichAlbum;
|
|
7
|
+
/** public `/track/` payload — the only source of BPM */
|
|
8
|
+
publicTrack?: trackTypePublicApi | null;
|
|
9
|
+
lyrics?: lyricsType | null;
|
|
10
|
+
cover?: Buffer | null;
|
|
11
|
+
artistImage?: Buffer | null;
|
|
12
|
+
coverSize: number;
|
|
13
|
+
/** write `DEEZER_*_ID`, `URL_REWRITING` slug, provider/label ids */
|
|
14
|
+
deezerIds: boolean;
|
|
15
|
+
/** write popularity rank */
|
|
16
|
+
includeRank: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface Person {
|
|
19
|
+
role: string;
|
|
20
|
+
name: string;
|
|
21
|
+
}
|
|
22
|
+
export interface TrackTagModel {
|
|
23
|
+
title: string;
|
|
24
|
+
subtitle?: string;
|
|
25
|
+
album: string;
|
|
26
|
+
artists: string[];
|
|
27
|
+
mainArtists: string[];
|
|
28
|
+
featuredArtists: string[];
|
|
29
|
+
albumArtist: string;
|
|
30
|
+
composers: string[];
|
|
31
|
+
lyricists: string[];
|
|
32
|
+
producers: string[];
|
|
33
|
+
engineers: Person[];
|
|
34
|
+
mixers: string[];
|
|
35
|
+
performers: Person[];
|
|
36
|
+
publishers: string[];
|
|
37
|
+
trackNumber: number;
|
|
38
|
+
trackTotal?: number;
|
|
39
|
+
discNumber: number;
|
|
40
|
+
discTotal?: number;
|
|
41
|
+
isrc?: string;
|
|
42
|
+
barcode?: string;
|
|
43
|
+
durationMs: number;
|
|
44
|
+
bpm?: number;
|
|
45
|
+
genres: string[];
|
|
46
|
+
label?: string;
|
|
47
|
+
releaseType?: string;
|
|
48
|
+
isCompilation: boolean;
|
|
49
|
+
date?: string;
|
|
50
|
+
year?: string;
|
|
51
|
+
originalDate?: string;
|
|
52
|
+
originalYear?: string;
|
|
53
|
+
copyright?: string;
|
|
54
|
+
producerLine?: string;
|
|
55
|
+
replayGainTrackGain?: string;
|
|
56
|
+
explicit: 'explicit' | 'clean' | 'unknown';
|
|
57
|
+
itunesAdvisory: number;
|
|
58
|
+
lyrics?: string;
|
|
59
|
+
lyricsSynced?: string;
|
|
60
|
+
lyricsWriters?: string;
|
|
61
|
+
lyricsCopyright?: string;
|
|
62
|
+
ids: {
|
|
63
|
+
deezerTrack?: string;
|
|
64
|
+
deezerAlbum?: string;
|
|
65
|
+
deezerArtist?: string;
|
|
66
|
+
slug?: string;
|
|
67
|
+
labelId?: string;
|
|
68
|
+
providerId?: string;
|
|
69
|
+
};
|
|
70
|
+
rank?: number;
|
|
71
|
+
cover?: Buffer | null;
|
|
72
|
+
coverSize: number;
|
|
73
|
+
artistImage?: Buffer | null;
|
|
74
|
+
}
|
|
75
|
+
export declare const buildTagModel: (input: TagModelInput) => TrackTagModel;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildTagModel = void 0;
|
|
4
|
+
const contributors_1 = require("./contributors");
|
|
5
|
+
const lrc_1 = require("./lrc");
|
|
6
|
+
const yearOf = (d) => (d && /^\d{4}/.test(d) ? d.slice(0, 4) : undefined);
|
|
7
|
+
const explicitFrom = (status) => {
|
|
8
|
+
// Deezer EXPLICIT_LYRICS_STATUS: 0 none · 1 explicit · 2 unknown · 3 edited/clean
|
|
9
|
+
// · 4 partially-explicit · 6 no-advice · 7 partially-no-advice
|
|
10
|
+
if (status === 1 || status === 4)
|
|
11
|
+
return { explicit: 'explicit', itunesAdvisory: 1 };
|
|
12
|
+
if (status === 3)
|
|
13
|
+
return { explicit: 'clean', itunesAdvisory: 2 };
|
|
14
|
+
return { explicit: 'unknown', itunesAdvisory: 0 };
|
|
15
|
+
};
|
|
16
|
+
const buildTagModel = (input) => {
|
|
17
|
+
var _a, _b, _c, _d, _e;
|
|
18
|
+
const { track, album, publicTrack, lyrics, coverSize } = input;
|
|
19
|
+
const c = (0, contributors_1.normalizeContributors)(track.SNG_CONTRIBUTORS);
|
|
20
|
+
// main artists: contributors first, else the ARTISTS[] list, else ART_NAME
|
|
21
|
+
const artistsAll = (track.ARTISTS || []).map((a) => a.ART_NAME).filter(Boolean);
|
|
22
|
+
const mainArtists = c.mainArtists.length ? c.mainArtists : artistsAll.length ? artistsAll : [track.ART_NAME];
|
|
23
|
+
const featuredArtists = c.featuring.length ? c.featuring : artistsAll.filter((n) => !mainArtists.includes(n));
|
|
24
|
+
const artists = [...new Set([...mainArtists, ...featuredArtists])];
|
|
25
|
+
const albMeta = album || { genres: [], isCompilation: false, isLive: false };
|
|
26
|
+
const isCompilation = albMeta.isCompilation || /^various/i.test(track.ART_NAME);
|
|
27
|
+
const date = albMeta.releaseDate ||
|
|
28
|
+
((publicTrack === null || publicTrack === void 0 ? void 0 : publicTrack.release_date) && /^\d{4}-\d{2}-\d{2}$/.test(publicTrack.release_date)
|
|
29
|
+
? publicTrack.release_date
|
|
30
|
+
: undefined);
|
|
31
|
+
const originalDate = albMeta.originalDate && albMeta.originalDate !== date ? albMeta.originalDate : undefined;
|
|
32
|
+
const gain = parseFloat((_c = (_a = track.GAIN) !== null && _a !== void 0 ? _a : (_b = publicTrack === null || publicTrack === void 0 ? void 0 : publicTrack.gain) === null || _b === void 0 ? void 0 : _b.toString()) !== null && _c !== void 0 ? _c : '');
|
|
33
|
+
const bpmRaw = (_d = publicTrack === null || publicTrack === void 0 ? void 0 : publicTrack.bpm) !== null && _d !== void 0 ? _d : 0;
|
|
34
|
+
const explicit = explicitFrom((_e = track.EXPLICIT_TRACK_CONTENT) === null || _e === void 0 ? void 0 : _e.EXPLICIT_LYRICS_STATUS);
|
|
35
|
+
let releaseType = albMeta.recordType;
|
|
36
|
+
if (albMeta.isLive)
|
|
37
|
+
releaseType = 'live';
|
|
38
|
+
else if (isCompilation)
|
|
39
|
+
releaseType = 'compilation';
|
|
40
|
+
const version = (track.VERSION || (publicTrack === null || publicTrack === void 0 ? void 0 : publicTrack.title_version) || '').trim();
|
|
41
|
+
return {
|
|
42
|
+
title: track.SNG_TITLE,
|
|
43
|
+
subtitle: version || undefined,
|
|
44
|
+
album: track.ALB_TITLE || albMeta.title,
|
|
45
|
+
artists,
|
|
46
|
+
mainArtists,
|
|
47
|
+
featuredArtists,
|
|
48
|
+
albumArtist: albMeta.albumArtist || track.ART_NAME,
|
|
49
|
+
composers: c.composers,
|
|
50
|
+
lyricists: c.lyricists,
|
|
51
|
+
producers: c.producers,
|
|
52
|
+
engineers: c.engineers,
|
|
53
|
+
mixers: c.mixers,
|
|
54
|
+
performers: c.performers,
|
|
55
|
+
publishers: c.publishers,
|
|
56
|
+
trackNumber: Number(track.TRACK_NUMBER) || 0,
|
|
57
|
+
trackTotal: albMeta.trackTotal,
|
|
58
|
+
discNumber: Number(track.DISK_NUMBER) || 1,
|
|
59
|
+
discTotal: albMeta.discTotal,
|
|
60
|
+
isrc: track.ISRC || (publicTrack === null || publicTrack === void 0 ? void 0 : publicTrack.isrc) || undefined,
|
|
61
|
+
barcode: albMeta.upc,
|
|
62
|
+
durationMs: (Number(track.DURATION) || 0) * 1000,
|
|
63
|
+
bpm: bpmRaw > 0 ? bpmRaw : undefined,
|
|
64
|
+
genres: albMeta.genres || [],
|
|
65
|
+
label: albMeta.label,
|
|
66
|
+
releaseType,
|
|
67
|
+
isCompilation,
|
|
68
|
+
date,
|
|
69
|
+
year: yearOf(date),
|
|
70
|
+
originalDate,
|
|
71
|
+
originalYear: yearOf(originalDate),
|
|
72
|
+
copyright: albMeta.copyright,
|
|
73
|
+
producerLine: albMeta.producerLine,
|
|
74
|
+
replayGainTrackGain: Number.isFinite(gain) ? `${gain.toFixed(2)} dB` : undefined,
|
|
75
|
+
explicit: explicit.explicit,
|
|
76
|
+
itunesAdvisory: explicit.itunesAdvisory,
|
|
77
|
+
lyrics: (lyrics === null || lyrics === void 0 ? void 0 : lyrics.LYRICS_TEXT) || undefined,
|
|
78
|
+
lyricsSynced: (0, lrc_1.toLrc)(lyrics === null || lyrics === void 0 ? void 0 : lyrics.LYRICS_SYNC_JSON, {
|
|
79
|
+
title: track.SNG_TITLE,
|
|
80
|
+
artist: mainArtists.join(', '),
|
|
81
|
+
album: track.ALB_TITLE,
|
|
82
|
+
writers: lyrics === null || lyrics === void 0 ? void 0 : lyrics.LYRICS_WRITERS,
|
|
83
|
+
length: Number(track.DURATION) || undefined,
|
|
84
|
+
}) || undefined,
|
|
85
|
+
lyricsWriters: (lyrics === null || lyrics === void 0 ? void 0 : lyrics.LYRICS_WRITERS) || undefined,
|
|
86
|
+
lyricsCopyright: (lyrics === null || lyrics === void 0 ? void 0 : lyrics.LYRICS_COPYRIGHTS) || undefined,
|
|
87
|
+
ids: input.deezerIds
|
|
88
|
+
? {
|
|
89
|
+
deezerTrack: track.SNG_ID,
|
|
90
|
+
deezerAlbum: track.ALB_ID,
|
|
91
|
+
deezerArtist: track.ART_ID,
|
|
92
|
+
slug: track.URL_REWRITING || undefined,
|
|
93
|
+
labelId: albMeta.labelId,
|
|
94
|
+
providerId: track.PROVIDER_ID || undefined,
|
|
95
|
+
}
|
|
96
|
+
: {},
|
|
97
|
+
rank: input.includeRank && track.RANK ? Number(track.RANK) || undefined : undefined,
|
|
98
|
+
cover: input.cover,
|
|
99
|
+
coverSize,
|
|
100
|
+
artistImage: input.artistImage,
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
exports.buildTagModel = buildTagModel;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { albumType, albumTypePublicApi } from '../types';
|
|
2
|
+
export interface RichAlbum {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
albumArtist: string;
|
|
6
|
+
/** © line — from gw `COPYRIGHT`, falls back to `PRODUCER_LINE` */
|
|
7
|
+
copyright?: string;
|
|
8
|
+
/** ℗ line — only set when it differs from `copyright` */
|
|
9
|
+
producerLine?: string;
|
|
10
|
+
/** best available release date, YYYY-MM-DD */
|
|
11
|
+
releaseDate?: string;
|
|
12
|
+
/** ORIGINAL_RELEASE_DATE — the true first release; only when it differs from releaseDate */
|
|
13
|
+
originalDate?: string;
|
|
14
|
+
upc?: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
labelId?: string;
|
|
17
|
+
genres: string[];
|
|
18
|
+
/** album | single | ep | compile | live … */
|
|
19
|
+
recordType?: string;
|
|
20
|
+
isCompilation: boolean;
|
|
21
|
+
isLive: boolean;
|
|
22
|
+
trackTotal?: number;
|
|
23
|
+
discTotal?: number;
|
|
24
|
+
fans?: number;
|
|
25
|
+
/** the two raw payloads, for callers that want more */
|
|
26
|
+
gw?: albumType;
|
|
27
|
+
publicApi?: albumTypePublicApi;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* One merged album view from the two endpoints that actually carry the good
|
|
31
|
+
* fields: `album.getData` (gw — ©/℗, original release date, disc total,
|
|
32
|
+
* subtypes) and the public `/album/` (label, genres, record type). Both calls
|
|
33
|
+
* are memoised and in-flight-coalesced by the api layer, so calling this once
|
|
34
|
+
* per track of an album costs a single network round-trip for the whole album.
|
|
35
|
+
*
|
|
36
|
+
* The public endpoint 404s for some small-label albums; that path is optional
|
|
37
|
+
* and the gw data still comes through.
|
|
38
|
+
*/
|
|
39
|
+
export declare const getRichAlbum: (albId: string) => Promise<RichAlbum>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getRichAlbum = void 0;
|
|
4
|
+
const api_1 = require("../api");
|
|
5
|
+
const pickDate = (s) => s && /^\d{4}-\d{2}-\d{2}$/.test(s) && !/^0000/.test(s) ? s : undefined;
|
|
6
|
+
/**
|
|
7
|
+
* One merged album view from the two endpoints that actually carry the good
|
|
8
|
+
* fields: `album.getData` (gw — ©/℗, original release date, disc total,
|
|
9
|
+
* subtypes) and the public `/album/` (label, genres, record type). Both calls
|
|
10
|
+
* are memoised and in-flight-coalesced by the api layer, so calling this once
|
|
11
|
+
* per track of an album costs a single network round-trip for the whole album.
|
|
12
|
+
*
|
|
13
|
+
* The public endpoint 404s for some small-label albums; that path is optional
|
|
14
|
+
* and the gw data still comes through.
|
|
15
|
+
*/
|
|
16
|
+
const getRichAlbum = async (albId) => {
|
|
17
|
+
var _a, _b, _c, _d, _e;
|
|
18
|
+
const [gwResult, publicResult] = await Promise.allSettled([(0, api_1.getAlbumInfo)(albId), (0, api_1.getAlbumInfoPublicApi)(albId)]);
|
|
19
|
+
const gw = gwResult.status === 'fulfilled' ? gwResult.value : undefined;
|
|
20
|
+
const pub = publicResult.status === 'fulfilled' ? publicResult.value : undefined;
|
|
21
|
+
const copyright = ((gw === null || gw === void 0 ? void 0 : gw.COPYRIGHT) || (gw === null || gw === void 0 ? void 0 : gw.PRODUCER_LINE) || '').trim() || undefined;
|
|
22
|
+
const producerLineRaw = ((gw === null || gw === void 0 ? void 0 : gw.PRODUCER_LINE) || '').trim() || undefined;
|
|
23
|
+
const producerLine = producerLineRaw && producerLineRaw !== copyright ? producerLineRaw : undefined;
|
|
24
|
+
const releaseDate = pickDate(gw === null || gw === void 0 ? void 0 : gw.DIGITAL_RELEASE_DATE) ||
|
|
25
|
+
pickDate(pub === null || pub === void 0 ? void 0 : pub.release_date) ||
|
|
26
|
+
pickDate(gw === null || gw === void 0 ? void 0 : gw.PHYSICAL_RELEASE_DATE) ||
|
|
27
|
+
pickDate(gw === null || gw === void 0 ? void 0 : gw.ORIGINAL_RELEASE_DATE);
|
|
28
|
+
const originalRaw = pickDate(gw === null || gw === void 0 ? void 0 : gw.ORIGINAL_RELEASE_DATE);
|
|
29
|
+
const originalDate = originalRaw && originalRaw !== releaseDate ? originalRaw : undefined;
|
|
30
|
+
const recordType = (pub === null || pub === void 0 ? void 0 : pub.record_type) || undefined;
|
|
31
|
+
return {
|
|
32
|
+
id: albId,
|
|
33
|
+
title: (gw === null || gw === void 0 ? void 0 : gw.ALB_TITLE) || (pub === null || pub === void 0 ? void 0 : pub.title) || '',
|
|
34
|
+
albumArtist: (gw === null || gw === void 0 ? void 0 : gw.ART_NAME) || ((_a = pub === null || pub === void 0 ? void 0 : pub.artist) === null || _a === void 0 ? void 0 : _a.name) || '',
|
|
35
|
+
copyright,
|
|
36
|
+
producerLine,
|
|
37
|
+
releaseDate,
|
|
38
|
+
originalDate,
|
|
39
|
+
upc: (gw === null || gw === void 0 ? void 0 : gw.UPC) || (pub === null || pub === void 0 ? void 0 : pub.upc) || undefined,
|
|
40
|
+
label: (pub === null || pub === void 0 ? void 0 : pub.label) || undefined,
|
|
41
|
+
labelId: (gw === null || gw === void 0 ? void 0 : gw.LABEL_ID) || undefined,
|
|
42
|
+
genres: (((_b = pub === null || pub === void 0 ? void 0 : pub.genres) === null || _b === void 0 ? void 0 : _b.data) || []).map((g) => g.name).filter(Boolean),
|
|
43
|
+
recordType,
|
|
44
|
+
isCompilation: Boolean((_c = gw === null || gw === void 0 ? void 0 : gw.SUBTYPES) === null || _c === void 0 ? void 0 : _c.isCompilation) || recordType === 'compile',
|
|
45
|
+
isLive: Boolean((_d = gw === null || gw === void 0 ? void 0 : gw.SUBTYPES) === null || _d === void 0 ? void 0 : _d.isLive),
|
|
46
|
+
trackTotal: (gw === null || gw === void 0 ? void 0 : gw.NUMBER_TRACK) ? Number(gw.NUMBER_TRACK) : pub === null || pub === void 0 ? void 0 : pub.nb_tracks,
|
|
47
|
+
discTotal: (gw === null || gw === void 0 ? void 0 : gw.NUMBER_DISK) ? Number(gw.NUMBER_DISK) : undefined,
|
|
48
|
+
fans: (_e = pub === null || pub === void 0 ? void 0 : pub.fans) !== null && _e !== void 0 ? _e : gw === null || gw === void 0 ? void 0 : gw.NB_FAN,
|
|
49
|
+
gw,
|
|
50
|
+
publicApi: pub,
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
exports.getRichAlbum = getRichAlbum;
|
package/dist/types/album.d.ts
CHANGED
|
@@ -21,8 +21,8 @@ export interface albumTypeMinimal {
|
|
|
21
21
|
}
|
|
22
22
|
export interface albumType {
|
|
23
23
|
ALB_CONTRIBUTORS: {
|
|
24
|
-
|
|
25
|
-
};
|
|
24
|
+
[role: string]: string[];
|
|
25
|
+
} | [];
|
|
26
26
|
ALB_ID: string;
|
|
27
27
|
ALB_PICTURE: string;
|
|
28
28
|
EXPLICIT_ALBUM_CONTENT: {
|
|
@@ -34,7 +34,9 @@ export interface albumType {
|
|
|
34
34
|
ART_ID: string;
|
|
35
35
|
ART_NAME: string;
|
|
36
36
|
ARTIST_IS_DUMMY: boolean;
|
|
37
|
+
COPYRIGHT?: string;
|
|
37
38
|
DIGITAL_RELEASE_DATE: string;
|
|
39
|
+
ORIGINAL_RELEASE_DATE?: string;
|
|
38
40
|
EXPLICIT_LYRICS?: string;
|
|
39
41
|
NB_FAN: number;
|
|
40
42
|
NUMBER_DISK: string;
|
|
@@ -45,6 +47,12 @@ export interface albumType {
|
|
|
45
47
|
RANK: string;
|
|
46
48
|
RANK_ART: string;
|
|
47
49
|
STATUS: string;
|
|
50
|
+
SUBTYPES?: {
|
|
51
|
+
isStudio: boolean;
|
|
52
|
+
isLive: boolean;
|
|
53
|
+
isCompilation: boolean;
|
|
54
|
+
isKaraoke: boolean;
|
|
55
|
+
};
|
|
48
56
|
TYPE: string;
|
|
49
57
|
UPC: string;
|
|
50
58
|
__TYPE__: 'album';
|
package/dist/types/tracks.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ interface mediaType {
|
|
|
3
3
|
TYPE: 'preview';
|
|
4
4
|
HREF: string;
|
|
5
5
|
}
|
|
6
|
-
interface lyricsSync {
|
|
6
|
+
export interface lyricsSync {
|
|
7
7
|
lrc_timestamp: string;
|
|
8
8
|
milliseconds: string;
|
|
9
9
|
duration: string;
|
|
@@ -16,6 +16,17 @@ export interface lyricsType {
|
|
|
16
16
|
LYRICS_COPYRIGHTS?: string;
|
|
17
17
|
LYRICS_WRITERS?: string;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* `SNG_CONTRIBUTORS` as Deezer actually ships it: a bag of role -> names.
|
|
21
|
+
* The role keys are inconsistent across the catalogue — `main_artist` vs
|
|
22
|
+
* `mainartist` vs `artist`, `musicpublisher` vs `music publisher` — and the
|
|
23
|
+
* whole thing is sometimes an empty array. Normalise with
|
|
24
|
+
* `normalizeContributors()` from `metadata-writer/contributors` rather than
|
|
25
|
+
* reading keys directly.
|
|
26
|
+
*/
|
|
27
|
+
export type sngContributors = {
|
|
28
|
+
[role: string]: string[];
|
|
29
|
+
} | [];
|
|
19
30
|
interface songType {
|
|
20
31
|
ALB_ID: string;
|
|
21
32
|
ALB_TITLE: string;
|
|
@@ -40,17 +51,7 @@ interface songType {
|
|
|
40
51
|
SMARTRADIO: string;
|
|
41
52
|
SNG_ID: string;
|
|
42
53
|
SNG_TITLE: string;
|
|
43
|
-
SNG_CONTRIBUTORS?:
|
|
44
|
-
main_artist: string[];
|
|
45
|
-
author?: string[];
|
|
46
|
-
composer?: string[];
|
|
47
|
-
musicpublisher?: string[];
|
|
48
|
-
producer?: string[];
|
|
49
|
-
publisher: string[];
|
|
50
|
-
engineer?: string[];
|
|
51
|
-
writer?: string[];
|
|
52
|
-
mixer?: string[];
|
|
53
|
-
} | [];
|
|
54
|
+
SNG_CONTRIBUTORS?: sngContributors;
|
|
54
55
|
STATUS: number;
|
|
55
56
|
S_MOD: number;
|
|
56
57
|
S_PREMIUM: number;
|