music-metadata 9.0.3 → 10.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.
@@ -3,7 +3,7 @@ import { ITag } from '../type.js';
3
3
  import { INativeMetadataCollector, IWarningCollector } from './MetadataCollector.js';
4
4
  export interface IGenericTagMapper {
5
5
  /**
6
- * Which tagType it able to map to the generic mapping format
6
+ * Which tagType is able to map to the generic mapping format
7
7
  */
8
8
  tagTypes: generic.TagType[];
9
9
  /**
@@ -167,6 +167,14 @@ export class MetadataCollector {
167
167
  if (this.common[tag.id] && this.common[tag.id].indexOf(tag.value) !== -1)
168
168
  return;
169
169
  break;
170
+ case 'comment':
171
+ if (typeof tag.value === 'string') {
172
+ tag.value = { text: tag.value };
173
+ }
174
+ if (tag.value.descriptor === 'iTunPGAP') {
175
+ this.setGenericTag(tagType, { id: 'gapless', value: tag.value.text === '1' });
176
+ }
177
+ break;
170
178
  default:
171
179
  // nothing to do
172
180
  }
package/lib/core.d.ts CHANGED
@@ -5,6 +5,7 @@ import * as strtok3 from 'strtok3';
5
5
  import type { IAudioMetadata, INativeTagDict, IOptions, IPicture, IPrivateOptions, IRandomReader, ITag } from './type.js';
6
6
  import type { ReadableStream as NodeReadableStream } from 'node:stream/web';
7
7
  export { IFileInfo } from 'strtok3';
8
+ export { IAudioMetadata, IOptions, ITag, INativeTagDict, ICommonTagsResult, IFormat, IPicture, IRatio, IChapter, ILyricsTag, LyricsContentType, TimestampFormat } from './type.js';
8
9
  export type AnyWebStream<G> = NodeReadableStream<G> | ReadableStream<G>;
9
10
  /**
10
11
  * Parse Web API File
package/lib/core.js CHANGED
@@ -7,6 +7,7 @@ import { RandomUint8ArrayReader } from './common/RandomUint8ArrayReader.js';
7
7
  import { APEv2Parser } from './apev2/APEv2Parser.js';
8
8
  import { hasID3v1Header } from './id3v1/ID3v1Parser.js';
9
9
  import { getLyricsHeaderLength } from './lyrics3/Lyrics3.js';
10
+ export { LyricsContentType, TimestampFormat } from './type.js';
10
11
  /**
11
12
  * Parse Web API File
12
13
  * Requires Blob to be able to stream using a ReadableStreamBYOBReader, only available since Node.js ≥ 20
@@ -1,4 +1,4 @@
1
- import { ID3v2MajorVersion } from './ID3v2Token.js';
1
+ import { ID3v2MajorVersion, ITextEncoding } from './ID3v2Token.js';
2
2
  import { IWarningCollector } from '../common/MetadataCollector.js';
3
3
  export declare function parseGenre(origVal: string): string[];
4
4
  export declare class FrameParser {
@@ -11,6 +11,10 @@ export declare class FrameParser {
11
11
  */
12
12
  constructor(major: ID3v2MajorVersion, warningCollector: IWarningCollector);
13
13
  readData(uint8Array: Uint8Array, type: string, includeCovers: boolean): any;
14
+ protected static readNullTerminatedString(uint8Array: Uint8Array, encoding: ITextEncoding): {
15
+ text: string;
16
+ len: number;
17
+ };
14
18
  protected static fixPictureMimeType(pictureType: string): string;
15
19
  /**
16
20
  * Converts TMCL (Musician credits list) or TIPL (Involved people list)
@@ -1,7 +1,7 @@
1
1
  import initDebug from 'debug';
2
2
  import * as Token from 'token-types';
3
3
  import * as util from '../common/Util.js';
4
- import { AttachedPictureType, TextEncodingToken } from './ID3v2Token.js';
4
+ import { AttachedPictureType, TextEncodingToken, SyncTextHeader, TextHeader } from './ID3v2Token.js';
5
5
  import { Genres } from '../id3v1/ID3v1Parser.js';
6
6
  const debug = initDebug('music-metadata:id3v2:frame-parser');
7
7
  const defaultEnc = 'latin1'; // latin1 == iso-8859-1;
@@ -75,7 +75,6 @@ export class FrameParser {
75
75
  let output = []; // ToDo
76
76
  const nullTerminatorLength = FrameParser.getNullTerminatorLength(encoding);
77
77
  let fzero;
78
- const out = {};
79
78
  debug(`Parsing tag type=${type}, encoding=${encoding}, bom=${bom}`);
80
79
  switch (type !== 'TXXX' && type[0] === 'T' ? 'T*' : type) {
81
80
  case 'T*': // 4.2.1. Text information frames - details
@@ -168,31 +167,49 @@ export class FrameParser {
168
167
  output = Token.UINT32_BE.get(uint8Array, 0);
169
168
  break;
170
169
  case 'SYLT':
171
- // skip text encoding (1 byte),
172
- // language (3 bytes),
173
- // time stamp format (1 byte),
174
- // content tagTypes (1 byte),
175
- // content descriptor (1 byte)
176
- offset += 7;
177
- output = [];
170
+ const syltHeader = SyncTextHeader.get(uint8Array, 0);
171
+ offset += SyncTextHeader.len;
172
+ const result = {
173
+ descriptor: '',
174
+ language: syltHeader.language,
175
+ contentType: syltHeader.contentType,
176
+ timeStampFormat: syltHeader.timeStampFormat,
177
+ syncText: []
178
+ };
179
+ let readSyllables = false;
178
180
  while (offset < length) {
179
- const txt = uint8Array.slice(offset, offset = util.findZero(uint8Array, offset, length, encoding));
180
- offset += 5; // push offset forward one + 4 byte timestamp
181
- output.push(util.decodeString(txt, encoding));
181
+ const nullStr = FrameParser.readNullTerminatedString(uint8Array.subarray(offset), syltHeader.encoding);
182
+ offset += nullStr.len;
183
+ if (readSyllables) {
184
+ const timestamp = Token.UINT32_BE.get(uint8Array, offset);
185
+ offset += Token.UINT32_BE.len;
186
+ result.syncText.push({
187
+ text: nullStr.text,
188
+ timestamp
189
+ });
190
+ }
191
+ else {
192
+ result.descriptor = nullStr.text;
193
+ readSyllables = true;
194
+ }
182
195
  }
196
+ output = result;
183
197
  break;
184
198
  case 'ULT':
185
199
  case 'USLT':
186
200
  case 'COM':
187
201
  case 'COMM':
188
- offset += 1;
189
- out.language = util.decodeString(uint8Array.slice(offset, offset + 3), defaultEnc);
190
- offset += 3;
191
- fzero = util.findZero(uint8Array, offset, length, encoding);
192
- out.description = util.decodeString(uint8Array.slice(offset, fzero), encoding);
193
- offset = fzero + nullTerminatorLength;
194
- out.text = util.decodeString(uint8Array.slice(offset, length), encoding).replace(/\x00+$/, '');
195
- output = [out];
202
+ const textHeader = TextHeader.get(uint8Array, offset);
203
+ offset += TextHeader.len;
204
+ const descriptorStr = FrameParser.readNullTerminatedString(uint8Array.subarray(offset), textHeader.encoding);
205
+ offset += descriptorStr.len;
206
+ const textStr = FrameParser.readNullTerminatedString(uint8Array.subarray(offset), textHeader.encoding);
207
+ const comment = {
208
+ language: textHeader.language,
209
+ descriptor: descriptorStr.text,
210
+ text: textStr.text
211
+ };
212
+ output = comment;
196
213
  break;
197
214
  case 'UFID':
198
215
  output = FrameParser.readIdentifierAndData(uint8Array, offset, length, defaultEnc);
@@ -265,6 +282,15 @@ export class FrameParser {
265
282
  }
266
283
  return output;
267
284
  }
285
+ static readNullTerminatedString(uint8Array, encoding) {
286
+ let offset = encoding.bom ? 2 : 0;
287
+ const txt = uint8Array.slice(offset, offset = util.findZero(uint8Array, offset, uint8Array.length, encoding.encoding));
288
+ offset += encoding.encoding === 'utf-16le' ? 2 : 1;
289
+ return {
290
+ text: util.decodeString(txt, encoding.encoding),
291
+ len: offset
292
+ };
293
+ }
268
294
  static fixPictureMimeType(pictureType) {
269
295
  pictureType = pictureType.toLocaleLowerCase();
270
296
  switch (pictureType) {
@@ -27,12 +27,6 @@ export const id3v22TagMap = {
27
27
  TEN: 'encodedby',
28
28
  TSS: 'encodersettings',
29
29
  WAR: 'website',
30
- 'COM:iTunPGAP': 'gapless'
31
- /* ToDo: iTunes tags:
32
- 'COM:iTunNORM': ,
33
- 'COM:iTunSMPB': 'encoder delay',
34
- 'COM:iTunes_CDDB_IDs'
35
- */ ,
36
30
  PCS: 'podcast',
37
31
  TCP: "compilation",
38
32
  TDR: 'date',
@@ -155,7 +155,6 @@ export class ID3v24TagMapper extends CaseInsensitiveTagMap {
155
155
  * @param warnings Wil be used to register (collect) warnings
156
156
  */
157
157
  postMap(tag, warnings) {
158
- var _a, _b;
159
158
  switch (tag.id) {
160
159
  case 'UFID': // decode MusicBrainz Recording Id
161
160
  if (tag.value.owner_identifier === 'http://musicbrainz.org') {
@@ -178,15 +177,9 @@ export class ID3v24TagMapper extends CaseInsensitiveTagMap {
178
177
  warnings.addWarning(`Unknown PRIV owner-identifier: ${tag.value.owner_identifier}`);
179
178
  }
180
179
  break;
181
- case 'COMM':
182
- tag.value = (_a = tag.value) === null || _a === void 0 ? void 0 : _a.text;
183
- break;
184
180
  case 'POPM':
185
181
  tag.value = ID3v24TagMapper.toRating(tag.value);
186
182
  break;
187
- case 'USLT':
188
- tag.value = (_b = tag.value) === null || _b === void 0 ? void 0 : _b.text;
189
- break;
190
183
  default:
191
184
  break;
192
185
  }
@@ -22,6 +22,7 @@ export declare class ID3v2Parser {
22
22
  parseExtendedHeader(): Promise<void>;
23
23
  parseExtendedHeaderData(dataRemaining: number, extendedHeaderSize: number): Promise<void>;
24
24
  parseId3Data(dataLen: number): Promise<void>;
25
+ private handleTag;
25
26
  private addTag;
26
27
  private parseMetadata;
27
28
  private readFrameHeader;
@@ -97,25 +97,20 @@ export class ID3v2Parser {
97
97
  async parseId3Data(dataLen) {
98
98
  const uint8Array = await this.tokenizer.readToken(new Token.Uint8ArrayType(dataLen));
99
99
  for (const tag of this.parseMetadata(uint8Array)) {
100
- if (tag.id === 'TXXX') {
101
- if (tag.value) {
102
- await Promise.all(tag.value.text.map(text => this.addTag(ID3v2Parser.makeDescriptionTagName(tag.id, tag.value.description), text)));
103
- }
104
- }
105
- else if (tag.id === 'COM') {
106
- await Promise.all(tag.value.map(value => this.addTag(ID3v2Parser.makeDescriptionTagName(tag.id, value.description), value.text)));
107
- }
108
- else if (tag.id === 'COMM') {
109
- await Promise.all(tag.value.map(value => this.addTag(ID3v2Parser.makeDescriptionTagName(tag.id, value.description), value)));
110
- }
111
- else if (Array.isArray(tag.value)) {
112
- await Promise.all(tag.value.map(value => this.addTag(tag.id, value)));
113
- }
114
- else {
115
- await this.addTag(tag.id, tag.value);
100
+ switch (tag.id) {
101
+ case 'TXXX':
102
+ if (tag.value) {
103
+ await this.handleTag(tag, tag.value.text, () => tag.value.description);
104
+ }
105
+ break;
106
+ default:
107
+ await (Array.isArray(tag.value) ? Promise.all(tag.value.map(value => this.addTag(tag.id, value))) : this.addTag(tag.id, tag.value));
116
108
  }
117
109
  }
118
110
  }
111
+ async handleTag(tag, values, descriptor, resolveValue = value => value) {
112
+ await Promise.all(values.map(value => this.addTag(ID3v2Parser.makeDescriptionTagName(tag.id, descriptor(value)), resolveValue(value))));
113
+ }
119
114
  async addTag(id, value) {
120
115
  await this.metadata.addTag(this.headerType, id, value);
121
116
  }
@@ -34,6 +34,23 @@ export interface IExtendedHeader {
34
34
  sizeOfPadding: number;
35
35
  crcDataPresent: boolean;
36
36
  }
37
+ /**
38
+ * https://id3.org/id3v2.3.0#Synchronised_lyrics.2Ftext
39
+ */
40
+ export declare enum LyricsContentType {
41
+ other = 0,
42
+ lyrics = 1,
43
+ text = 2,
44
+ movement_part = 3,
45
+ events = 4,
46
+ chord = 5,
47
+ trivia_pop = 6
48
+ }
49
+ export declare enum TimestampFormat {
50
+ notSynchronized0 = 0,
51
+ mpegFrameNumber = 1,
52
+ milliseconds = 2
53
+ }
37
54
  /**
38
55
  * 28 bits (representing up to 256MB) integer, the msb is 0 to avoid 'false syncsignals'.
39
56
  * 4 * %0xxxxxxx
@@ -71,3 +88,25 @@ export interface ITextEncoding {
71
88
  bom?: boolean;
72
89
  }
73
90
  export declare const TextEncodingToken: IGetToken<ITextEncoding>;
91
+ /**
92
+ * `USLT` frame fields
93
+ */
94
+ export interface ITextHeader {
95
+ encoding: ITextEncoding;
96
+ language: string;
97
+ }
98
+ /**
99
+ * Used to read first portion of `SYLT` frame
100
+ */
101
+ export declare const TextHeader: IGetToken<ITextHeader>;
102
+ /**
103
+ * SYLT` frame fields
104
+ */
105
+ export interface ISyncTextHeader extends ITextHeader {
106
+ contentType: LyricsContentType;
107
+ timeStampFormat: TimestampFormat;
108
+ }
109
+ /**
110
+ * Used to read first portion of `SYLT` frame
111
+ */
112
+ export declare const SyncTextHeader: IGetToken<ISyncTextHeader>;
@@ -28,6 +28,25 @@ export var AttachedPictureType;
28
28
  AttachedPictureType[AttachedPictureType["Band/artist logotype"] = 19] = "Band/artist logotype";
29
29
  AttachedPictureType[AttachedPictureType["Publisher/Studio logotype"] = 20] = "Publisher/Studio logotype";
30
30
  })(AttachedPictureType || (AttachedPictureType = {}));
31
+ /**
32
+ * https://id3.org/id3v2.3.0#Synchronised_lyrics.2Ftext
33
+ */
34
+ export var LyricsContentType;
35
+ (function (LyricsContentType) {
36
+ LyricsContentType[LyricsContentType["other"] = 0] = "other";
37
+ LyricsContentType[LyricsContentType["lyrics"] = 1] = "lyrics";
38
+ LyricsContentType[LyricsContentType["text"] = 2] = "text";
39
+ LyricsContentType[LyricsContentType["movement_part"] = 3] = "movement_part";
40
+ LyricsContentType[LyricsContentType["events"] = 4] = "events";
41
+ LyricsContentType[LyricsContentType["chord"] = 5] = "chord";
42
+ LyricsContentType[LyricsContentType["trivia_pop"] = 6] = "trivia_pop";
43
+ })(LyricsContentType || (LyricsContentType = {}));
44
+ export var TimestampFormat;
45
+ (function (TimestampFormat) {
46
+ TimestampFormat[TimestampFormat["notSynchronized0"] = 0] = "notSynchronized0";
47
+ TimestampFormat[TimestampFormat["mpegFrameNumber"] = 1] = "mpegFrameNumber";
48
+ TimestampFormat[TimestampFormat["milliseconds"] = 2] = "milliseconds";
49
+ })(TimestampFormat || (TimestampFormat = {}));
31
50
  /**
32
51
  * 28 bits (representing up to 256MB) integer, the msb is 0 to avoid 'false syncsignals'.
33
52
  * 4 * %0xxxxxxx
@@ -101,3 +120,31 @@ export const TextEncodingToken = {
101
120
  }
102
121
  }
103
122
  };
123
+ /**
124
+ * Used to read first portion of `SYLT` frame
125
+ */
126
+ export const TextHeader = {
127
+ len: 4,
128
+ get: (uint8Array, off) => {
129
+ return {
130
+ encoding: TextEncodingToken.get(uint8Array, off),
131
+ language: new Token.StringType(3, 'latin1').get(uint8Array, off + 1)
132
+ };
133
+ }
134
+ };
135
+ /**
136
+ * Used to read first portion of `SYLT` frame
137
+ */
138
+ export const SyncTextHeader = {
139
+ len: 6,
140
+ get: (uint8Array, off) => {
141
+ const text = TextHeader.get(uint8Array, off);
142
+ return {
143
+ encoding: text.encoding,
144
+ language: text.language,
145
+ timeStampFormat: Token.UINT8.get(uint8Array, off + 4),
146
+ contentType: Token.UINT8.get(uint8Array, off + 5)
147
+ };
148
+ }
149
+ };
150
+ //# sourceMappingURL=ID3v2Token.js.map
package/lib/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  import * as Stream from 'stream';
5
5
  import * as strtok3 from 'strtok3';
6
6
  import { IAudioMetadata, IOptions } from './type.js';
7
- export { IAudioMetadata, IOptions, ITag, INativeTagDict, ICommonTagsResult, IFormat, IPicture, IRatio, IChapter } from './type.js';
7
+ export { IAudioMetadata, IOptions, ITag, INativeTagDict, ICommonTagsResult, IFormat, IPicture, IRatio, IChapter, ILyricsTag, LyricsContentType, TimestampFormat } from './type.js';
8
8
  export { parseFromTokenizer, parseBuffer, parseBlob, parseWebStream, selectCover, orderTags, ratingToStars, IFileInfo } from './core.js';
9
9
  /**
10
10
  * Parse audio from Node Stream.Readable
package/lib/index.js CHANGED
@@ -6,6 +6,7 @@ import initDebug from 'debug';
6
6
  import { parseFromTokenizer, scanAppendingHeaders } from './core.js';
7
7
  import { ParserFactory } from './ParserFactory.js';
8
8
  import { RandomFileReader } from './common/RandomFileReader.js';
9
+ export { LyricsContentType, TimestampFormat } from './type.js';
9
10
  export { parseFromTokenizer, parseBuffer, parseBlob, parseWebStream, selectCover, orderTags, ratingToStars } from './core.js';
10
11
  const debug = initDebug('music-metadata:parser');
11
12
  /**
package/lib/type.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { GenericTagId, TagType } from './common/GenericTagTypes.js';
2
2
  import { IFooter } from './apev2/APEv2Token.js';
3
3
  import { TrackType } from './matroska/types.js';
4
+ import { LyricsContentType, TimestampFormat } from './id3v2/ID3v2Token.js';
4
5
  export { TrackType } from './matroska/types.js';
6
+ export { LyricsContentType, TimestampFormat } from './id3v2/ID3v2Token.js';
5
7
  /**
6
8
  * Attached picture, typically used for cover art
7
9
  */
@@ -92,7 +94,7 @@ export interface ICommonTagsResult {
92
94
  /**
93
95
  * List of comments
94
96
  */
95
- comment?: string[];
97
+ comment?: IComment[];
96
98
  /**
97
99
  * Genre
98
100
  */
@@ -106,9 +108,9 @@ export interface ICommonTagsResult {
106
108
  */
107
109
  composer?: string[];
108
110
  /**
109
- * Lyrics
111
+ * Synchronized lyrics
110
112
  */
111
- lyrics?: string[];
113
+ lyrics?: ILyricsTag[];
112
114
  /**
113
115
  * Album title, formatted for alphabetic ordering
114
116
  */
@@ -593,3 +595,24 @@ export interface IRandomReader {
593
595
  */
594
596
  randomRead(buffer: Uint8Array, offset: number, length: number, position: number): Promise<number>;
595
597
  }
598
+ interface ILyricsText {
599
+ text: string;
600
+ timestamp?: number;
601
+ }
602
+ export interface IComment {
603
+ descriptor?: string;
604
+ language?: string;
605
+ text?: string;
606
+ }
607
+ export interface ILyricsTag extends IComment {
608
+ contentType: LyricsContentType;
609
+ timeStampFormat: TimestampFormat;
610
+ /**
611
+ * Un-synchronized lyrics
612
+ */
613
+ text?: string;
614
+ /**
615
+ * Synchronized lyrics
616
+ */
617
+ syncText: ILyricsText[];
618
+ }
package/lib/type.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { TrackType } from './matroska/types.js';
2
+ export { LyricsContentType, TimestampFormat } from './id3v2/ID3v2Token.js';
2
3
  //# sourceMappingURL=type.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "music-metadata",
3
3
  "description": "Music metadata parser for Node.js, supporting virtual any audio and tag format.",
4
- "version": "9.0.3",
4
+ "version": "10.0.0",
5
5
  "author": {
6
6
  "name": "Borewit",
7
7
  "url": "https://github.com/Borewit"
@@ -84,7 +84,7 @@
84
84
  "compile-test": "tsc -p test",
85
85
  "compile-doc": "tsc -p doc-gen",
86
86
  "compile": "npm run compile-src && npm run compile-test && npm run compile-doc",
87
- "eslint": "eslint lib/**/*.ts --ignore-pattern lib/**/*.d.ts example/typescript/**/*.ts test/**/*.ts doc-gen/**/*.ts",
87
+ "eslint": "eslint lib test doc-gen",
88
88
  "lint-md": "remark -u preset-lint-markdown-style-guide .",
89
89
  "lint": "npm run lint-md && npm run eslint",
90
90
  "test": "mocha",
@@ -105,25 +105,29 @@
105
105
  "uint8array-extras": "^1.4.0"
106
106
  },
107
107
  "devDependencies": {
108
+ "@eslint/compat": "^1.1.1",
109
+ "@eslint/eslintrc": "^3.1.0",
110
+ "@eslint/js": "^9.7.0",
108
111
  "@types/chai": "^4.3.16",
109
112
  "@types/chai-as-promised": "^7.1.8",
110
113
  "@types/debug": "^4.1.12",
111
114
  "@types/file-type": "^10.9.1",
112
115
  "@types/mocha": "^10.0.7",
113
116
  "@types/node": "^20.14.11",
114
- "@typescript-eslint/eslint-plugin": "^7.16.0",
115
- "@typescript-eslint/parser": "^7.16.0",
117
+ "@typescript-eslint/eslint-plugin": "^7.16.1",
118
+ "@typescript-eslint/parser": "^7.16.1",
116
119
  "c8": "^10.1.2",
117
120
  "chai": "^5.1.1",
118
121
  "chai-as-promised": "^8.0.0",
119
122
  "del-cli": "^5.1.0",
120
- "eslint": "^8.57.0",
123
+ "eslint": "^9.7.0",
121
124
  "eslint-config-prettier": "^9.1.0",
122
125
  "eslint-import-resolver-typescript": "^3.6.1",
123
126
  "eslint-plugin-import": "^2.29.1",
124
127
  "eslint-plugin-jsdoc": "^48.7.0",
125
128
  "eslint-plugin-node": "^11.1.0",
126
129
  "eslint-plugin-unicorn": "^54.0.0",
130
+ "globals": "^15.8.0",
127
131
  "mime": "^4.0.4",
128
132
  "mocha": "^10.6.0",
129
133
  "npm-run-all": "^4.1.5",