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
|
@@ -3,35 +3,49 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.downloadAlbumCover = void 0;
|
|
6
|
+
exports.downloadArtistImage = exports.downloadAlbumCover = exports.MAX_COVER_SIZE = void 0;
|
|
7
7
|
const fast_lru_1 = __importDefault(require("../lib/fast-lru"));
|
|
8
8
|
const http_1 = require("../lib/http");
|
|
9
|
+
exports.MAX_COVER_SIZE = 1800;
|
|
9
10
|
// expire cache in 30 minutes
|
|
10
11
|
const lru = new fast_lru_1.default({
|
|
11
12
|
maxSize: 50,
|
|
12
13
|
ttl: 30 * 60000,
|
|
13
14
|
});
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*/
|
|
19
|
-
const downloadAlbumCover = async (track, albumCoverSize) => {
|
|
20
|
-
if (!track.ALB_PICTURE) {
|
|
15
|
+
const clampSize = (n) => Math.max(56, Math.min(exports.MAX_COVER_SIZE, Math.round(n) || 500));
|
|
16
|
+
const imageUrl = (kind, md5, size) => `https://e-cdns-images.dzcdn.net/images/${kind}/${md5}/${size}x${size}-000000-80-0-0.jpg`;
|
|
17
|
+
const fetchImage = async (kind, md5, size) => {
|
|
18
|
+
if (!md5) {
|
|
21
19
|
return null;
|
|
22
20
|
}
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
const px = clampSize(size);
|
|
22
|
+
const key = `${kind}:${md5}:${px}`;
|
|
23
|
+
const cached = lru.get(key);
|
|
24
|
+
if (cached) {
|
|
25
|
+
return cached;
|
|
26
26
|
}
|
|
27
27
|
try {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
lru.set(track.ALB_PICTURE + albumCoverSize, data);
|
|
28
|
+
const data = await (0, http_1.getBuffer)(imageUrl(kind, md5, px));
|
|
29
|
+
lru.set(key, data);
|
|
31
30
|
return data;
|
|
32
31
|
}
|
|
33
32
|
catch (err) {
|
|
34
33
|
return null;
|
|
35
34
|
}
|
|
36
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* @param track track info json from deezer api
|
|
38
|
+
* @param albumCoverSize in pixels, 56–1800 (clamped)
|
|
39
|
+
*/
|
|
40
|
+
const downloadAlbumCover = (track, albumCoverSize) => fetchImage('cover', track.ALB_PICTURE, albumCoverSize);
|
|
37
41
|
exports.downloadAlbumCover = downloadAlbumCover;
|
|
42
|
+
/**
|
|
43
|
+
* Artist photo (`ART_PICTURE` md5). Returns null when the track has no artist
|
|
44
|
+
* image (common for small-catalogue artists).
|
|
45
|
+
*/
|
|
46
|
+
const downloadArtistImage = (track, size = 1000) => {
|
|
47
|
+
var _a, _b;
|
|
48
|
+
const md5 = track.ART_PICTURE || ((_b = (_a = track.ARTISTS) === null || _a === void 0 ? void 0 : _a.find((a) => a.ART_PICTURE)) === null || _b === void 0 ? void 0 : _b.ART_PICTURE);
|
|
49
|
+
return fetchImage('artist', md5, size);
|
|
50
|
+
};
|
|
51
|
+
exports.downloadArtistImage = downloadArtistImage;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { sngContributors } from '../types';
|
|
2
|
+
export interface NormalizedContributors {
|
|
3
|
+
/** billed / lead artists */
|
|
4
|
+
mainArtists: string[];
|
|
5
|
+
/** "feat." artists */
|
|
6
|
+
featuring: string[];
|
|
7
|
+
composers: string[];
|
|
8
|
+
/** author / writer / lyricist, de-duplicated */
|
|
9
|
+
lyricists: string[];
|
|
10
|
+
producers: string[];
|
|
11
|
+
/** {role, name} for mastering / mixing / recording (+ "second") engineers */
|
|
12
|
+
engineers: {
|
|
13
|
+
role: string;
|
|
14
|
+
name: string;
|
|
15
|
+
}[];
|
|
16
|
+
mixers: string[];
|
|
17
|
+
/** other performers Deezer credits by role, e.g. background vocalist */
|
|
18
|
+
performers: {
|
|
19
|
+
role: string;
|
|
20
|
+
name: string;
|
|
21
|
+
}[];
|
|
22
|
+
publishers: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Turn Deezer's inconsistent `SNG_CONTRIBUTORS` bag into a stable shape.
|
|
26
|
+
*
|
|
27
|
+
* The role keys vary across the catalogue (`main_artist` / `mainartist` /
|
|
28
|
+
* `artist`, `musicpublisher` / `music publisher`, …) and the value is
|
|
29
|
+
* occasionally an empty array — this hides all of that.
|
|
30
|
+
*/
|
|
31
|
+
export declare const normalizeContributors: (raw: sngContributors | undefined) => NormalizedContributors;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeContributors = void 0;
|
|
4
|
+
/** collapse `Music Publisher`, `music_publisher`, `musicpublisher` → `musicpublisher` */
|
|
5
|
+
const canon = (key) => key.toLowerCase().replace(/[\s_-]+/g, '');
|
|
6
|
+
const ENGINEER_LABELS = {
|
|
7
|
+
masteringengineer: 'mastering engineer',
|
|
8
|
+
mixingengineer: 'mixing engineer',
|
|
9
|
+
recordingengineer: 'recording engineer',
|
|
10
|
+
recordingsecondengineer: 'assistant recording engineer',
|
|
11
|
+
assistantengineer: 'assistant engineer',
|
|
12
|
+
engineer: 'engineer',
|
|
13
|
+
studiopersonnel: 'engineer',
|
|
14
|
+
};
|
|
15
|
+
const uniq = (arr) => [...new Set(arr.filter((x) => x && x.trim()))].map((x) => x.trim());
|
|
16
|
+
/**
|
|
17
|
+
* Turn Deezer's inconsistent `SNG_CONTRIBUTORS` bag into a stable shape.
|
|
18
|
+
*
|
|
19
|
+
* The role keys vary across the catalogue (`main_artist` / `mainartist` /
|
|
20
|
+
* `artist`, `musicpublisher` / `music publisher`, …) and the value is
|
|
21
|
+
* occasionally an empty array — this hides all of that.
|
|
22
|
+
*/
|
|
23
|
+
const normalizeContributors = (raw) => {
|
|
24
|
+
const out = {
|
|
25
|
+
mainArtists: [],
|
|
26
|
+
featuring: [],
|
|
27
|
+
composers: [],
|
|
28
|
+
lyricists: [],
|
|
29
|
+
producers: [],
|
|
30
|
+
engineers: [],
|
|
31
|
+
mixers: [],
|
|
32
|
+
performers: [],
|
|
33
|
+
publishers: [],
|
|
34
|
+
};
|
|
35
|
+
if (!raw || Array.isArray(raw)) {
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
const bucket = {};
|
|
39
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
40
|
+
if (!Array.isArray(value)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const k = canon(key);
|
|
44
|
+
bucket[k] = (bucket[k] || []).concat(value);
|
|
45
|
+
}
|
|
46
|
+
out.mainArtists = uniq([...(bucket.mainartist || []), ...(bucket.artist || [])]);
|
|
47
|
+
out.featuring = uniq([...(bucket.featuring || []), ...(bucket.featuredartist || []), ...(bucket.feat || [])]);
|
|
48
|
+
out.composers = uniq(bucket.composer || []);
|
|
49
|
+
out.lyricists = uniq([
|
|
50
|
+
...(bucket.author || []),
|
|
51
|
+
...(bucket.writer || []),
|
|
52
|
+
...(bucket.lyricist || []),
|
|
53
|
+
...(bucket.songwriter || []),
|
|
54
|
+
]);
|
|
55
|
+
out.producers = uniq([...(bucket.producer || []), ...(bucket.coproducer || []), ...(bucket.executiveproducer || [])]);
|
|
56
|
+
out.mixers = uniq([...(bucket.mixer || []), ...(bucket.remixer || [])]);
|
|
57
|
+
out.publishers = uniq([
|
|
58
|
+
...(bucket.publisher || []),
|
|
59
|
+
...(bucket.musicpublisher || []),
|
|
60
|
+
...(bucket.originalpublisher || []),
|
|
61
|
+
]);
|
|
62
|
+
for (const [canonKey, label] of Object.entries(ENGINEER_LABELS)) {
|
|
63
|
+
for (const name of uniq(bucket[canonKey] || [])) {
|
|
64
|
+
out.engineers.push({ role: label, name });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const [canonKey, names] of Object.entries(bucket)) {
|
|
68
|
+
if (canonKey === 'mainartist' ||
|
|
69
|
+
canonKey === 'artist' ||
|
|
70
|
+
canonKey === 'featuring' ||
|
|
71
|
+
canonKey === 'composer' ||
|
|
72
|
+
canonKey === 'mixer' ||
|
|
73
|
+
canonKey === 'producer' ||
|
|
74
|
+
canonKey in ENGINEER_LABELS ||
|
|
75
|
+
/publisher|author|writer|lyricist/.test(canonKey)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// remaining roles: vocalist, backgroundvocalist, instruments, arranger, …
|
|
79
|
+
const role = canonKey.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
80
|
+
for (const name of uniq(names)) {
|
|
81
|
+
out.performers.push({ role, name });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
};
|
|
86
|
+
exports.normalizeContributors = normalizeContributors;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
import type {
|
|
3
|
-
export
|
|
2
|
+
import type { TrackTagModel } from './model';
|
|
3
|
+
export interface FlacWriteOptions {
|
|
4
|
+
/** embed the LRC document as a `SYNCEDLYRICS` Vorbis comment. default true */
|
|
5
|
+
embedSyncedLyrics?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Write Vorbis comments + PICTURE blocks from the canonical model. Vorbis
|
|
9
|
+
* comments are free-form, so every field Deezer gives us lands here — including
|
|
10
|
+
* multi-valued credits (one comment per value) and synced lyrics.
|
|
11
|
+
*/
|
|
12
|
+
export declare const writeMetadataFlac: (buffer: Buffer, m: TrackTagModel, options?: FlacWriteOptions) => Buffer;
|
|
@@ -5,76 +5,101 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.writeMetadataFlac = void 0;
|
|
7
7
|
const metaflac_js_1 = __importDefault(require("../lib/metaflac-js"));
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Write Vorbis comments + PICTURE blocks from the canonical model. Vorbis
|
|
10
|
+
* comments are free-form, so every field Deezer gives us lands here — including
|
|
11
|
+
* multi-valued credits (one comment per value) and synced lyrics.
|
|
12
|
+
*/
|
|
13
|
+
const writeMetadataFlac = (buffer, m, options = {}) => {
|
|
9
14
|
const flac = new metaflac_js_1.default(buffer);
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
15
|
+
const tag = (key, value) => {
|
|
16
|
+
if (value !== undefined && value !== null && value !== '')
|
|
17
|
+
flac.setTag(`${key}=${value}`);
|
|
18
|
+
};
|
|
19
|
+
const multi = (key, values) => {
|
|
20
|
+
for (const v of values)
|
|
21
|
+
if (v)
|
|
22
|
+
flac.setTag(`${key}=${v}`);
|
|
23
|
+
};
|
|
24
|
+
tag('TITLE', m.title);
|
|
25
|
+
tag('SUBTITLE', m.subtitle);
|
|
26
|
+
if (m.subtitle)
|
|
27
|
+
tag('VERSION', m.subtitle);
|
|
28
|
+
tag('ALBUM', m.album);
|
|
29
|
+
multi('ARTIST', m.artists);
|
|
30
|
+
multi('ARTISTS', m.artists);
|
|
31
|
+
tag('ALBUMARTIST', m.albumArtist);
|
|
32
|
+
multi('COMPOSER', m.composers);
|
|
33
|
+
multi('LYRICIST', m.lyricists);
|
|
34
|
+
multi('WRITER', m.lyricists);
|
|
35
|
+
multi('PRODUCER', m.producers);
|
|
36
|
+
multi('MIXER', [...m.mixers, ...m.engineers.filter((e) => e.role.startsWith('mixing')).map((e) => e.name)]);
|
|
37
|
+
multi('PUBLISHER', m.publishers);
|
|
38
|
+
for (const e of m.engineers)
|
|
39
|
+
tag('ENGINEER', e.name);
|
|
40
|
+
// one human-readable line keeping the role breakdown (Picard-style)
|
|
41
|
+
for (const e of m.engineers)
|
|
42
|
+
tag('CREDITS', `${e.name} (${e.role})`);
|
|
43
|
+
for (const p of m.performers)
|
|
44
|
+
tag('PERFORMER', `${p.name} (${p.role})`);
|
|
45
|
+
multi('FEATURING', m.featuredArtists);
|
|
46
|
+
tag('TRACKNUMBER', m.trackNumber || '');
|
|
47
|
+
tag('TRACKTOTAL', m.trackTotal);
|
|
48
|
+
tag('TOTALTRACKS', m.trackTotal);
|
|
49
|
+
tag('DISCNUMBER', m.discNumber || '');
|
|
50
|
+
tag('DISCTOTAL', m.discTotal);
|
|
51
|
+
tag('TOTALDISCS', m.discTotal);
|
|
52
|
+
tag('ISRC', m.isrc);
|
|
53
|
+
tag('BARCODE', m.barcode);
|
|
54
|
+
tag('LENGTH', Math.round(m.durationMs / 1000) || '');
|
|
55
|
+
if (m.bpm)
|
|
56
|
+
tag('BPM', m.bpm);
|
|
57
|
+
tag('MEDIA', 'Digital Media');
|
|
58
|
+
multi('GENRE', m.genres);
|
|
59
|
+
tag('LABEL', m.label);
|
|
60
|
+
tag('ORGANIZATION', m.label);
|
|
61
|
+
tag('RELEASETYPE', m.releaseType);
|
|
62
|
+
tag('COMPILATION', m.isCompilation ? '1' : '0');
|
|
63
|
+
tag('DATE', m.date);
|
|
64
|
+
tag('YEAR', m.year);
|
|
65
|
+
tag('ORIGINALDATE', m.originalDate);
|
|
66
|
+
tag('ORIGINALYEAR', m.originalYear);
|
|
67
|
+
tag('COPYRIGHT', m.copyright);
|
|
68
|
+
tag('PRODUCERLINE', m.producerLine);
|
|
69
|
+
tag('REPLAYGAIN_TRACK_GAIN', m.replayGainTrackGain);
|
|
70
|
+
tag('ITUNESADVISORY', m.itunesAdvisory);
|
|
71
|
+
if (m.explicit !== 'unknown')
|
|
72
|
+
tag('EXPLICIT', m.explicit === 'explicit' ? '1' : '0');
|
|
73
|
+
// `LYRICS` carries the synced LRC when we have it (players auto-detect the
|
|
74
|
+
// timestamps); `UNSYNCEDLYRICS` is always the plain fallback.
|
|
75
|
+
const synced = m.lyricsSynced && options.embedSyncedLyrics !== false ? m.lyricsSynced : null;
|
|
76
|
+
if (synced || m.lyrics) {
|
|
77
|
+
tag('LYRICS', synced || m.lyrics);
|
|
31
78
|
}
|
|
32
|
-
if (
|
|
33
|
-
|
|
79
|
+
if (m.lyrics) {
|
|
80
|
+
tag('UNSYNCEDLYRICS', m.lyrics);
|
|
34
81
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
82
|
+
tag('LYRICS_WRITER', m.lyricsWriters);
|
|
83
|
+
tag('LYRICS_COPYRIGHT', m.lyricsCopyright);
|
|
84
|
+
if (m.rank !== undefined)
|
|
85
|
+
tag('DEEZER_RANK', m.rank);
|
|
86
|
+
tag('SOURCE', 'Deezer');
|
|
87
|
+
if (m.ids.deezerTrack) {
|
|
88
|
+
tag('SOURCEID', m.ids.deezerTrack);
|
|
89
|
+
tag('DEEZER_TRACK_ID', m.ids.deezerTrack);
|
|
40
90
|
}
|
|
41
|
-
|
|
42
|
-
|
|
91
|
+
tag('DEEZER_ALBUM_ID', m.ids.deezerAlbum);
|
|
92
|
+
tag('DEEZER_ARTIST_ID', m.ids.deezerArtist);
|
|
93
|
+
tag('DEEZER_LABEL_ID', m.ids.labelId);
|
|
94
|
+
tag('DEEZER_PROVIDER_ID', m.ids.providerId);
|
|
95
|
+
if (m.ids.slug)
|
|
96
|
+
tag('WEBSITE', `https://www.deezer.com/track/${m.ids.deezerTrack}`);
|
|
97
|
+
if (m.cover) {
|
|
98
|
+
flac.importPicture(m.cover, m.coverSize, 'image/jpeg', 3, 'Front cover');
|
|
43
99
|
}
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
flac.setTag(`COPYRIGHT=${RELEASE_YEAR ? RELEASE_YEAR + ' ' : ''}${track.SNG_CONTRIBUTORS.main_artist[0]}`);
|
|
47
|
-
}
|
|
48
|
-
if (track.SNG_CONTRIBUTORS.publisher) {
|
|
49
|
-
flac.setTag('ORGANIZATION=' + track.SNG_CONTRIBUTORS.publisher.join(', '));
|
|
50
|
-
}
|
|
51
|
-
if (track.SNG_CONTRIBUTORS.composer) {
|
|
52
|
-
flac.setTag('COMPOSER=' + track.SNG_CONTRIBUTORS.composer.join(', '));
|
|
53
|
-
}
|
|
54
|
-
if (track.SNG_CONTRIBUTORS.publisher) {
|
|
55
|
-
flac.setTag('ORGANIZATION=' + track.SNG_CONTRIBUTORS.publisher.join(', '));
|
|
56
|
-
}
|
|
57
|
-
if (track.SNG_CONTRIBUTORS.producer) {
|
|
58
|
-
flac.setTag('PRODUCER=' + track.SNG_CONTRIBUTORS.producer.join(', '));
|
|
59
|
-
}
|
|
60
|
-
if (track.SNG_CONTRIBUTORS.engineer) {
|
|
61
|
-
flac.setTag('ENGINEER=' + track.SNG_CONTRIBUTORS.engineer.join(', '));
|
|
62
|
-
}
|
|
63
|
-
if (track.SNG_CONTRIBUTORS.writer) {
|
|
64
|
-
flac.setTag('WRITER=' + track.SNG_CONTRIBUTORS.writer.join(', '));
|
|
65
|
-
}
|
|
66
|
-
if (track.SNG_CONTRIBUTORS.author) {
|
|
67
|
-
flac.setTag('AUTHOR=' + track.SNG_CONTRIBUTORS.author.join(', '));
|
|
68
|
-
}
|
|
69
|
-
if (track.SNG_CONTRIBUTORS.mixer) {
|
|
70
|
-
flac.setTag('MIXER=' + track.SNG_CONTRIBUTORS.mixer.join(', '));
|
|
71
|
-
}
|
|
100
|
+
if (m.artistImage) {
|
|
101
|
+
flac.importPicture(m.artistImage, 1000, 'image/jpeg', 8, 'Artist');
|
|
72
102
|
}
|
|
73
|
-
if (cover) {
|
|
74
|
-
flac.importPicture(cover, dimension, 'image/jpeg');
|
|
75
|
-
}
|
|
76
|
-
flac.setTag('SOURCE=Deezer');
|
|
77
|
-
flac.setTag('SOURCEID=' + track.SNG_ID);
|
|
78
103
|
return flac.getBuffer();
|
|
79
104
|
};
|
|
80
105
|
exports.writeMetadataFlac = writeMetadataFlac;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
import type {
|
|
3
|
-
|
|
2
|
+
import type { TrackTagModel } from './model';
|
|
3
|
+
/**
|
|
4
|
+
* Write an ID3v2.3 tag from the canonical model. `browser-id3-writer` is a v2.3
|
|
5
|
+
* writer, so there is no `SYLT` (synced lyrics) or `TDRC`/`TDOR` — synced lyrics
|
|
6
|
+
* ship as a `.lrc` sidecar from the CLI, and dates use `TYER`/`TDAT` plus
|
|
7
|
+
* `TXXX:ORIGINALDATE`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const writeMetadataMp3: (buffer: Buffer, m: TrackTagModel) => Buffer;
|
|
@@ -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;
|