gerdur-core 1.0.4 → 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 +51 -0
- package/README.md +17 -9
- 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
|
@@ -6,114 +6,121 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.writeMetadataMp3 = void 0;
|
|
7
7
|
// @ts-ignore
|
|
8
8
|
const browser_id3_writer_1 = __importDefault(require("browser-id3-writer"));
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
9
|
+
const join = (arr) => arr.filter(Boolean).join('; ');
|
|
10
|
+
/**
|
|
11
|
+
* Write an ID3v2.3 tag from the canonical model. `browser-id3-writer` is a v2.3
|
|
12
|
+
* writer, so there is no `SYLT` (synced lyrics) or `TDRC`/`TDOR` — synced lyrics
|
|
13
|
+
* ship as a `.lrc` sidecar from the CLI, and dates use `TYER`/`TDAT` plus
|
|
14
|
+
* `TXXX:ORIGINALDATE`.
|
|
15
|
+
*/
|
|
16
|
+
const writeMetadataMp3 = (buffer, m) => {
|
|
17
|
+
const w = new browser_id3_writer_1.default(buffer);
|
|
18
|
+
const txxx = (description, value) => {
|
|
19
|
+
const v = Array.isArray(value) ? join(value) : value;
|
|
20
|
+
if (v)
|
|
21
|
+
w.setFrame('TXXX', { description, value: v });
|
|
22
|
+
};
|
|
23
|
+
w.setFrame('TIT2', m.title);
|
|
24
|
+
if (m.subtitle)
|
|
25
|
+
w.setFrame('TIT3', m.subtitle);
|
|
26
|
+
w.setFrame('TALB', m.album);
|
|
27
|
+
if (m.artists.length)
|
|
28
|
+
w.setFrame('TPE1', m.artists);
|
|
29
|
+
if (m.albumArtist)
|
|
30
|
+
w.setFrame('TPE2', m.albumArtist);
|
|
31
|
+
if (m.composers.length)
|
|
32
|
+
w.setFrame('TCOM', m.composers);
|
|
33
|
+
if (m.lyricists.length)
|
|
34
|
+
w.setFrame('TEXT', join(m.lyricists));
|
|
35
|
+
if (m.mixers.length)
|
|
36
|
+
w.setFrame('TPE4', join(m.mixers));
|
|
37
|
+
if (m.genres.length)
|
|
38
|
+
w.setFrame('TCON', m.genres);
|
|
39
|
+
if (m.label)
|
|
40
|
+
w.setFrame('TPUB', m.label);
|
|
41
|
+
if (m.isrc)
|
|
42
|
+
w.setFrame('TSRC', m.isrc);
|
|
43
|
+
if (m.durationMs)
|
|
44
|
+
w.setFrame('TLEN', m.durationMs);
|
|
45
|
+
if (m.bpm)
|
|
46
|
+
w.setFrame('TBPM', Math.round(m.bpm));
|
|
47
|
+
w.setFrame('TMED', 'Digital Media');
|
|
48
|
+
if (m.trackNumber) {
|
|
49
|
+
w.setFrame('TRCK', m.trackTotal ? `${m.trackNumber}/${m.trackTotal}` : String(m.trackNumber));
|
|
43
50
|
}
|
|
44
|
-
|
|
45
|
-
.setFrame('
|
|
46
|
-
.setFrame('TXXX', {
|
|
47
|
-
description: 'SOURCE',
|
|
48
|
-
value: 'Deezer',
|
|
49
|
-
})
|
|
50
|
-
.setFrame('TXXX', {
|
|
51
|
-
description: 'SOURCEID',
|
|
52
|
-
value: track.SNG_ID,
|
|
53
|
-
});
|
|
54
|
-
if (track.DISK_NUMBER) {
|
|
55
|
-
const TRACK_NUMBER = track.TRACK_NUMBER.toLocaleString('en-US', { minimumIntegerDigits: 2 });
|
|
56
|
-
writer.setFrame('TPOS', track.DISK_NUMBER).setFrame('TRCK', album
|
|
57
|
-
? `${TRACK_NUMBER}/${album.nb_tracks.toLocaleString('en-US', {
|
|
58
|
-
minimumIntegerDigits: 2,
|
|
59
|
-
})}`
|
|
60
|
-
: TRACK_NUMBER);
|
|
51
|
+
if (m.discNumber) {
|
|
52
|
+
w.setFrame('TPOS', m.discTotal ? `${m.discNumber}/${m.discTotal}` : String(m.discNumber));
|
|
61
53
|
}
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
writer.setFrame('TPUB', track.SNG_CONTRIBUTORS.publisher.join(', '));
|
|
68
|
-
}
|
|
69
|
-
if (track.SNG_CONTRIBUTORS.composer) {
|
|
70
|
-
writer.setFrame('TCOM', track.SNG_CONTRIBUTORS.composer);
|
|
71
|
-
}
|
|
72
|
-
if (track.SNG_CONTRIBUTORS.writer) {
|
|
73
|
-
writer.setFrame('TXXX', {
|
|
74
|
-
description: 'LYRICIST',
|
|
75
|
-
value: track.SNG_CONTRIBUTORS.writer.join(', '),
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
if (track.SNG_CONTRIBUTORS.author) {
|
|
79
|
-
writer.setFrame('TXXX', {
|
|
80
|
-
description: 'AUTHOR',
|
|
81
|
-
value: track.SNG_CONTRIBUTORS.author.join(', '),
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
if (track.SNG_CONTRIBUTORS.mixer) {
|
|
85
|
-
writer.setFrame('TXXX', {
|
|
86
|
-
description: 'MIXARTIST',
|
|
87
|
-
value: track.SNG_CONTRIBUTORS.mixer.join(', '),
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
if (track.SNG_CONTRIBUTORS.producer && track.SNG_CONTRIBUTORS.engineer) {
|
|
91
|
-
writer.setFrame('TXXX', {
|
|
92
|
-
description: 'INVOLVEDPEOPLE',
|
|
93
|
-
value: track.SNG_CONTRIBUTORS.producer.concat(track.SNG_CONTRIBUTORS.engineer).join(', '),
|
|
94
|
-
});
|
|
95
|
-
}
|
|
54
|
+
if (m.year)
|
|
55
|
+
w.setFrame('TYER', Number(m.year));
|
|
56
|
+
if (m.date && /^\d{4}-\d{2}-\d{2}$/.test(m.date)) {
|
|
57
|
+
const [, mm, dd] = m.date.split('-');
|
|
58
|
+
w.setFrame('TDAT', Number(dd + mm));
|
|
96
59
|
}
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
60
|
+
if (m.copyright)
|
|
61
|
+
w.setFrame('TCOP', m.copyright);
|
|
62
|
+
// v2.3 has no native frames for these — user-defined text, Picard-compatible keys
|
|
63
|
+
txxx('DATE', m.date || '');
|
|
64
|
+
txxx('ORIGINALDATE', m.originalDate || '');
|
|
65
|
+
txxx('ORIGINALYEAR', m.originalYear || '');
|
|
66
|
+
txxx('BARCODE', m.barcode || '');
|
|
67
|
+
if (m.bpm)
|
|
68
|
+
txxx('BPM', String(m.bpm));
|
|
69
|
+
txxx('RELEASETYPE', m.releaseType || '');
|
|
70
|
+
txxx('COMPILATION', m.isCompilation ? '1' : '0');
|
|
71
|
+
txxx('ITUNESADVISORY', String(m.itunesAdvisory));
|
|
72
|
+
if (m.explicit !== 'unknown')
|
|
73
|
+
txxx('EXPLICIT', m.explicit === 'explicit' ? '1' : '0');
|
|
74
|
+
if (m.replayGainTrackGain)
|
|
75
|
+
txxx('REPLAYGAIN_TRACK_GAIN', m.replayGainTrackGain);
|
|
76
|
+
if (m.artists.length > 1)
|
|
77
|
+
txxx('ARTISTS', m.artists);
|
|
78
|
+
if (m.featuredArtists.length)
|
|
79
|
+
txxx('FEATURING', m.featuredArtists);
|
|
80
|
+
txxx('PRODUCER', m.producers);
|
|
81
|
+
txxx('MIXER', m.mixers);
|
|
82
|
+
txxx('PUBLISHER', m.publishers);
|
|
83
|
+
if (m.engineers.length) {
|
|
84
|
+
txxx('ENGINEER', m.engineers.map((e) => e.name));
|
|
85
|
+
txxx('INVOLVEDPEOPLE', m.engineers.map((e) => `${e.name} (${e.role})`));
|
|
102
86
|
}
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
description: 'EXPLICIT',
|
|
106
|
-
value: track.EXPLICIT_LYRICS,
|
|
107
|
-
});
|
|
87
|
+
if (m.performers.length) {
|
|
88
|
+
txxx('PERFORMER', m.performers.map((p) => `${p.name} (${p.role})`));
|
|
108
89
|
}
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
90
|
+
if (m.lyricsWriters)
|
|
91
|
+
txxx('LYRICIST', m.lyricsWriters);
|
|
92
|
+
if (m.lyricsCopyright)
|
|
93
|
+
txxx('LYRICS_COPYRIGHT', m.lyricsCopyright);
|
|
94
|
+
if (m.producerLine)
|
|
95
|
+
txxx('PRODUCERLINE', m.producerLine);
|
|
96
|
+
if (m.rank !== undefined)
|
|
97
|
+
txxx('DEEZER_RANK', String(m.rank));
|
|
98
|
+
txxx('SOURCE', 'Deezer');
|
|
99
|
+
if (m.ids.deezerTrack) {
|
|
100
|
+
txxx('SOURCEID', m.ids.deezerTrack);
|
|
101
|
+
txxx('DEEZER_TRACK_ID', m.ids.deezerTrack);
|
|
102
|
+
w.setFrame('WOAS', `https://www.deezer.com/track/${m.ids.deezerTrack}`);
|
|
115
103
|
}
|
|
116
|
-
|
|
117
|
-
|
|
104
|
+
if (m.ids.deezerAlbum)
|
|
105
|
+
txxx('DEEZER_ALBUM_ID', m.ids.deezerAlbum);
|
|
106
|
+
if (m.ids.deezerArtist) {
|
|
107
|
+
txxx('DEEZER_ARTIST_ID', m.ids.deezerArtist);
|
|
108
|
+
w.setFrame('WOAR', `https://www.deezer.com/artist/${m.ids.deezerArtist}`);
|
|
109
|
+
}
|
|
110
|
+
if (m.ids.labelId)
|
|
111
|
+
txxx('DEEZER_LABEL_ID', m.ids.labelId);
|
|
112
|
+
if (m.ids.providerId)
|
|
113
|
+
txxx('DEEZER_PROVIDER_ID', m.ids.providerId);
|
|
114
|
+
if (m.lyrics) {
|
|
115
|
+
w.setFrame('USLT', { description: '', lyrics: m.lyrics, language: 'eng' });
|
|
116
|
+
}
|
|
117
|
+
if (m.cover) {
|
|
118
|
+
w.setFrame('APIC', { type: 3, data: m.cover, description: 'Front cover' });
|
|
119
|
+
}
|
|
120
|
+
if (m.artistImage) {
|
|
121
|
+
w.setFrame('APIC', { type: 8, data: m.artistImage, description: 'Artist' });
|
|
122
|
+
}
|
|
123
|
+
w.addTag();
|
|
124
|
+
return Buffer.isBuffer(w.arrayBuffer) ? w.arrayBuffer : Buffer.from(w.arrayBuffer);
|
|
118
125
|
};
|
|
119
126
|
exports.writeMetadataMp3 = writeMetadataMp3;
|
|
@@ -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;
|