metadata-connect 1.1.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +196 -0
  3. package/dist/extract.d.ts +36 -0
  4. package/dist/extract.d.ts.map +1 -0
  5. package/dist/extract.js +77 -0
  6. package/dist/extract.js.map +1 -0
  7. package/dist/index.d.ts +6 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +8 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/parsers/aiff.d.ts +9 -0
  12. package/dist/parsers/aiff.d.ts.map +1 -0
  13. package/dist/parsers/aiff.js +42 -0
  14. package/dist/parsers/aiff.js.map +1 -0
  15. package/dist/parsers/flac.d.ts +6 -0
  16. package/dist/parsers/flac.d.ts.map +1 -0
  17. package/dist/parsers/flac.js +172 -0
  18. package/dist/parsers/flac.js.map +1 -0
  19. package/dist/parsers/id3.d.ts +6 -0
  20. package/dist/parsers/id3.d.ts.map +1 -0
  21. package/dist/parsers/id3.js +329 -0
  22. package/dist/parsers/id3.js.map +1 -0
  23. package/dist/parsers/index.d.ts +6 -0
  24. package/dist/parsers/index.d.ts.map +1 -0
  25. package/dist/parsers/index.js +6 -0
  26. package/dist/parsers/index.js.map +1 -0
  27. package/dist/parsers/mp4.d.ts +6 -0
  28. package/dist/parsers/mp4.d.ts.map +1 -0
  29. package/dist/parsers/mp4.js +295 -0
  30. package/dist/parsers/mp4.js.map +1 -0
  31. package/dist/parsers/utils.d.ts +40 -0
  32. package/dist/parsers/utils.d.ts.map +1 -0
  33. package/dist/parsers/utils.js +120 -0
  34. package/dist/parsers/utils.js.map +1 -0
  35. package/dist/reader.d.ts +6 -0
  36. package/dist/reader.d.ts.map +1 -0
  37. package/dist/reader.js +14 -0
  38. package/dist/reader.js.map +1 -0
  39. package/dist/types.d.ts +70 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +28 -0
  42. package/dist/types.js.map +1 -0
  43. package/package.json +75 -0
@@ -0,0 +1,329 @@
1
+ import { PictureType } from '../types.js';
2
+ import { detectImageType, readSyncsafe, getTextEncoding, readNullTerminatedString, parseBpm, parseYear, cleanText, } from './utils.js';
3
+ /**
4
+ * ID3v2 frame IDs for metadata extraction
5
+ */
6
+ const FRAME_IDS = {
7
+ // Text frames
8
+ TITLE: ['TIT2', 'TT2'], // Title
9
+ ARTIST: ['TPE1', 'TP1'], // Lead artist
10
+ ALBUM: ['TALB', 'TAL'], // Album
11
+ GENRE: ['TCON', 'TCO'], // Genre
12
+ YEAR: ['TYER', 'TYE', 'TDRC'], // Year (TYER for v2.3, TDRC for v2.4)
13
+ BPM: ['TBPM', 'TBP'], // BPM
14
+ KEY: ['TKEY', 'TKE'], // Musical key
15
+ // Picture frame
16
+ PICTURE: ['APIC', 'PIC'],
17
+ };
18
+ /**
19
+ * Parse an APIC (attached picture) frame
20
+ */
21
+ function parseApicFrame(data) {
22
+ if (data.length < 4)
23
+ return null;
24
+ let offset = 0;
25
+ const encodingByte = data[offset++];
26
+ let encoding = getTextEncoding(encodingByte);
27
+ // Check for BOM in UTF-16
28
+ if (encodingByte === 1 && data.length > offset + 2) {
29
+ const bom = data.readUInt16BE(offset);
30
+ if (bom === 0xfeff)
31
+ encoding = 'utf16be';
32
+ else if (bom === 0xfffe)
33
+ encoding = 'utf16le';
34
+ }
35
+ const mimeResult = readNullTerminatedString(data, offset, 'latin1');
36
+ const mimeType = mimeResult.value;
37
+ offset += mimeResult.bytesConsumed;
38
+ if (offset >= data.length)
39
+ return null;
40
+ const pictureType = data[offset++];
41
+ if (offset >= data.length)
42
+ return null;
43
+ const descResult = readNullTerminatedString(data, offset, encoding);
44
+ offset += descResult.bytesConsumed;
45
+ if (offset >= data.length)
46
+ return null;
47
+ const imageData = data.subarray(offset);
48
+ if (imageData.length === 0)
49
+ return null;
50
+ const detectedType = detectImageType(imageData);
51
+ const finalMimeType = detectedType ?? (mimeType.includes('png') ? 'image/png' : 'image/jpeg');
52
+ return { data: imageData, mimeType: finalMimeType, pictureType };
53
+ }
54
+ /**
55
+ * Parse a text frame (TIT2, TPE1, etc.)
56
+ */
57
+ function parseTextFrame(data) {
58
+ if (data.length < 2)
59
+ return undefined;
60
+ const encodingByte = data[0];
61
+ let encoding = getTextEncoding(encodingByte);
62
+ let offset = 1;
63
+ // Check for BOM in UTF-16
64
+ if (encodingByte === 1 && data.length > offset + 2) {
65
+ const bom = data.readUInt16BE(offset);
66
+ if (bom === 0xfeff) {
67
+ encoding = 'utf16be';
68
+ offset += 2;
69
+ }
70
+ else if (bom === 0xfffe) {
71
+ encoding = 'utf16le';
72
+ offset += 2;
73
+ }
74
+ }
75
+ // Read until null terminator or end of data
76
+ const textData = data.subarray(offset);
77
+ if (encoding === 'utf16le' || encoding === 'utf16be') {
78
+ // Find null terminator (two zeros)
79
+ let end = textData.length;
80
+ for (let i = 0; i < textData.length - 1; i += 2) {
81
+ if (textData[i] === 0 && textData[i + 1] === 0) {
82
+ end = i;
83
+ break;
84
+ }
85
+ }
86
+ if (encoding === 'utf16be') {
87
+ // Swap bytes for big-endian
88
+ const swapped = Buffer.alloc(end);
89
+ for (let i = 0; i < end; i += 2) {
90
+ swapped[i] = textData[i + 1];
91
+ swapped[i + 1] = textData[i];
92
+ }
93
+ return cleanText(swapped.toString('utf16le'));
94
+ }
95
+ return cleanText(textData.toString('utf16le', 0, end));
96
+ }
97
+ // Latin1 or UTF-8
98
+ let end = textData.indexOf(0);
99
+ if (end === -1)
100
+ end = textData.length;
101
+ return cleanText(textData.toString(encoding, 0, end));
102
+ }
103
+ /**
104
+ * Check if a frame ID matches any of the target IDs
105
+ */
106
+ function matchesFrameId(frameId, targets) {
107
+ return targets.some((target) => frameId === target || frameId.startsWith(target));
108
+ }
109
+ /**
110
+ * Extract metadata from an MP3 file with ID3v2 tags
111
+ */
112
+ export async function extractFromMp3(reader) {
113
+ // Read ID3v2 header (10 bytes)
114
+ const header = await reader.read(0, 10);
115
+ if (header.length < 10 || header.toString('ascii', 0, 3) !== 'ID3') {
116
+ return null;
117
+ }
118
+ const majorVersion = header[3];
119
+ const flags = header[5];
120
+ const tagSize = readSyncsafe(header, 6);
121
+ // Handle extended header if present
122
+ let extendedHeaderSize = 0;
123
+ if (flags & 0x40) {
124
+ const extHeader = await reader.read(10, 4);
125
+ extendedHeaderSize =
126
+ majorVersion === 4 ? readSyncsafe(extHeader, 0) : extHeader.readUInt32BE(0);
127
+ }
128
+ // Read all tag data
129
+ const tagData = await reader.read(10 + extendedHeaderSize, tagSize - extendedHeaderSize);
130
+ const metadata = {};
131
+ let frontCover = null;
132
+ let anyArtwork = null;
133
+ // Parse frames
134
+ let offset = 0;
135
+ const frameHeaderSize = majorVersion >= 3 ? 10 : 6;
136
+ while (offset < tagData.length - frameHeaderSize) {
137
+ // End of frames (padding starts with zero)
138
+ if (tagData[offset] === 0)
139
+ break;
140
+ let frameId;
141
+ let frameSize;
142
+ if (majorVersion >= 3) {
143
+ // ID3v2.3 or ID3v2.4
144
+ frameId = tagData.toString('ascii', offset, offset + 4);
145
+ frameSize =
146
+ majorVersion === 4
147
+ ? readSyncsafe(tagData, offset + 4)
148
+ : tagData.readUInt32BE(offset + 4);
149
+ }
150
+ else {
151
+ // ID3v2.2
152
+ frameId = tagData.toString('ascii', offset, offset + 3);
153
+ frameSize =
154
+ (tagData[offset + 3] << 16) | (tagData[offset + 4] << 8) | tagData[offset + 5];
155
+ }
156
+ if (frameSize <= 0 || frameSize > tagData.length - offset)
157
+ break;
158
+ const frameData = tagData.subarray(offset + frameHeaderSize, offset + frameHeaderSize + frameSize);
159
+ // Normalize v2.2 frame IDs to v2.3/4 equivalents
160
+ const normalizedId = majorVersion >= 3 ? frameId : normalizeV22FrameId(frameId);
161
+ // Extract metadata from text frames
162
+ if (matchesFrameId(normalizedId, FRAME_IDS.TITLE)) {
163
+ metadata.title = metadata.title ?? parseTextFrame(frameData);
164
+ }
165
+ else if (matchesFrameId(normalizedId, FRAME_IDS.ARTIST)) {
166
+ metadata.artist = metadata.artist ?? parseTextFrame(frameData);
167
+ }
168
+ else if (matchesFrameId(normalizedId, FRAME_IDS.ALBUM)) {
169
+ metadata.album = metadata.album ?? parseTextFrame(frameData);
170
+ }
171
+ else if (matchesFrameId(normalizedId, FRAME_IDS.GENRE)) {
172
+ metadata.genre = metadata.genre ?? parseGenre(parseTextFrame(frameData));
173
+ }
174
+ else if (matchesFrameId(normalizedId, FRAME_IDS.YEAR)) {
175
+ metadata.year = metadata.year ?? parseYear(parseTextFrame(frameData) ?? '');
176
+ }
177
+ else if (matchesFrameId(normalizedId, FRAME_IDS.BPM)) {
178
+ metadata.bpm = metadata.bpm ?? parseBpm(parseTextFrame(frameData) ?? '');
179
+ }
180
+ else if (matchesFrameId(normalizedId, FRAME_IDS.KEY)) {
181
+ metadata.key = metadata.key ?? parseTextFrame(frameData);
182
+ }
183
+ else if (matchesFrameId(normalizedId, FRAME_IDS.PICTURE)) {
184
+ const artwork = parseApicFrame(frameData);
185
+ if (artwork) {
186
+ if (artwork.pictureType === PictureType.FrontCover) {
187
+ frontCover = artwork;
188
+ }
189
+ else if (!anyArtwork) {
190
+ anyArtwork = artwork;
191
+ }
192
+ }
193
+ }
194
+ offset += frameHeaderSize + frameSize;
195
+ }
196
+ // Use front cover if available, otherwise any artwork
197
+ const artwork = frontCover ?? anyArtwork;
198
+ if (artwork) {
199
+ metadata.artwork = artwork.data;
200
+ metadata.artworkMimeType = artwork.mimeType;
201
+ }
202
+ // Return null if no metadata was found
203
+ if (Object.keys(metadata).length === 0) {
204
+ return null;
205
+ }
206
+ return metadata;
207
+ }
208
+ /**
209
+ * Normalize ID3v2.2 frame IDs to v2.3/4 equivalents
210
+ */
211
+ function normalizeV22FrameId(frameId) {
212
+ const mapping = {
213
+ TT2: 'TIT2', // Title
214
+ TP1: 'TPE1', // Artist
215
+ TAL: 'TALB', // Album
216
+ TCO: 'TCON', // Genre
217
+ TYE: 'TYER', // Year
218
+ TBP: 'TBPM', // BPM
219
+ TKE: 'TKEY', // Key
220
+ PIC: 'APIC', // Picture
221
+ };
222
+ return mapping[frameId] ?? frameId;
223
+ }
224
+ /**
225
+ * Parse genre string, handling ID3v1 numeric references
226
+ * Format: "(17)Rock" or "(17)" or "Rock"
227
+ */
228
+ function parseGenre(genre) {
229
+ if (!genre)
230
+ return undefined;
231
+ // Check for ID3v1 numeric reference
232
+ const match = genre.match(/^\((\d+)\)(.*)$/);
233
+ if (match) {
234
+ const id = parseInt(match[1], 10);
235
+ const textGenre = match[2].trim();
236
+ // If there's text after the number, use that
237
+ if (textGenre)
238
+ return textGenre;
239
+ // Otherwise look up the ID3v1 genre
240
+ return ID3V1_GENRES[id] ?? genre;
241
+ }
242
+ return genre;
243
+ }
244
+ /**
245
+ * ID3v1 genre list
246
+ */
247
+ const ID3V1_GENRES = {
248
+ 0: 'Blues',
249
+ 1: 'Classic Rock',
250
+ 2: 'Country',
251
+ 3: 'Dance',
252
+ 4: 'Disco',
253
+ 5: 'Funk',
254
+ 6: 'Grunge',
255
+ 7: 'Hip-Hop',
256
+ 8: 'Jazz',
257
+ 9: 'Metal',
258
+ 10: 'New Age',
259
+ 11: 'Oldies',
260
+ 12: 'Other',
261
+ 13: 'Pop',
262
+ 14: 'R&B',
263
+ 15: 'Rap',
264
+ 16: 'Reggae',
265
+ 17: 'Rock',
266
+ 18: 'Techno',
267
+ 19: 'Industrial',
268
+ 20: 'Alternative',
269
+ 21: 'Ska',
270
+ 22: 'Death Metal',
271
+ 23: 'Pranks',
272
+ 24: 'Soundtrack',
273
+ 25: 'Euro-Techno',
274
+ 26: 'Ambient',
275
+ 27: 'Trip-Hop',
276
+ 28: 'Vocal',
277
+ 29: 'Jazz+Funk',
278
+ 30: 'Fusion',
279
+ 31: 'Trance',
280
+ 32: 'Classical',
281
+ 33: 'Instrumental',
282
+ 34: 'Acid',
283
+ 35: 'House',
284
+ 36: 'Game',
285
+ 37: 'Sound Clip',
286
+ 38: 'Gospel',
287
+ 39: 'Noise',
288
+ 40: 'Alternative Rock',
289
+ 41: 'Bass',
290
+ 42: 'Soul',
291
+ 43: 'Punk',
292
+ 44: 'Space',
293
+ 45: 'Meditative',
294
+ 46: 'Instrumental Pop',
295
+ 47: 'Instrumental Rock',
296
+ 48: 'Ethnic',
297
+ 49: 'Gothic',
298
+ 50: 'Darkwave',
299
+ 51: 'Techno-Industrial',
300
+ 52: 'Electronic',
301
+ 53: 'Pop-Folk',
302
+ 54: 'Eurodance',
303
+ 55: 'Dream',
304
+ 56: 'Southern Rock',
305
+ 57: 'Comedy',
306
+ 58: 'Cult',
307
+ 59: 'Gangsta',
308
+ 60: 'Top 40',
309
+ 61: 'Christian Rap',
310
+ 62: 'Pop/Funk',
311
+ 63: 'Jungle',
312
+ 64: 'Native US',
313
+ 65: 'Cabaret',
314
+ 66: 'New Wave',
315
+ 67: 'Psychedelic',
316
+ 68: 'Rave',
317
+ 69: 'Showtunes',
318
+ 70: 'Trailer',
319
+ 71: 'Lo-Fi',
320
+ 72: 'Tribal',
321
+ 73: 'Acid Punk',
322
+ 74: 'Acid Jazz',
323
+ 75: 'Polka',
324
+ 76: 'Retro',
325
+ 77: 'Musical',
326
+ 78: 'Rock & Roll',
327
+ 79: 'Hard Rock',
328
+ };
329
+ //# sourceMappingURL=id3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id3.js","sourceRoot":"","sources":["../../src/parsers/id3.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export { extractFromMp3 } from './id3.js';
2
+ export { extractFromMp4 } from './mp4.js';
3
+ export { extractFromFlac } from './flac.js';
4
+ export { extractFromAiff } from './aiff.js';
5
+ export { detectImageType, normalizeMimeType } from './utils.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/parsers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { extractFromMp3 } from './id3.js';
2
+ export { extractFromMp4 } from './mp4.js';
3
+ export { extractFromFlac } from './flac.js';
4
+ export { extractFromAiff } from './aiff.js';
5
+ export { detectImageType, normalizeMimeType } from './utils.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/parsers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { ExtractedMetadata, FileReader } from '../types.js';
2
+ /**
3
+ * Extract metadata from an MP4/M4A file
4
+ */
5
+ export declare function extractFromMp4(reader: FileReader): Promise<ExtractedMetadata | null>;
6
+ //# sourceMappingURL=mp4.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mp4.d.ts","sourceRoot":"","sources":["../../src/parsers/mp4.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAmB,MAAM,aAAa,CAAC;AAsLlF;;GAEG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAiG1F"}
@@ -0,0 +1,295 @@
1
+ import { detectImageType, cleanText } from './utils.js';
2
+ /**
3
+ * iTunes metadata atom types
4
+ */
5
+ const ATOM_TYPES = {
6
+ TITLE: '\xa9nam', // Title
7
+ ARTIST: '\xa9ART', // Artist
8
+ ALBUM: '\xa9alb', // Album
9
+ GENRE: '\xa9gen', // Genre (text)
10
+ GENRE_ID: 'gnre', // Genre (ID3v1 numeric)
11
+ YEAR: '\xa9day', // Release date/year
12
+ BPM: 'tmpo', // BPM (tempo)
13
+ COVER: 'covr', // Cover artwork
14
+ // Additional useful atoms
15
+ ALBUM_ARTIST: 'aART', // Album artist
16
+ COMPOSER: '\xa9wrt', // Composer
17
+ COMMENT: '\xa9cmt', // Comment
18
+ };
19
+ /**
20
+ * Read an MP4 atom header
21
+ */
22
+ async function readAtomHeader(reader, offset) {
23
+ if (offset + 8 > reader.size)
24
+ return null;
25
+ const header = await reader.read(offset, 8);
26
+ const size = header.readUInt32BE(0);
27
+ const type = header.toString('ascii', 4, 8);
28
+ // Extended size (64-bit)
29
+ if (size === 1) {
30
+ if (offset + 16 > reader.size)
31
+ return null;
32
+ const extHeader = await reader.read(offset + 8, 8);
33
+ const extSize = Number(extHeader.readBigUInt64BE(0));
34
+ return { size: extSize, type, headerSize: 16 };
35
+ }
36
+ // Size 0 means atom extends to end of file
37
+ if (size === 0) {
38
+ return { size: reader.size - offset, type, headerSize: 8 };
39
+ }
40
+ return { size, type, headerSize: 8 };
41
+ }
42
+ /**
43
+ * Find an atom within a range
44
+ */
45
+ async function findAtom(reader, startOffset, endOffset, targetType) {
46
+ let offset = startOffset;
47
+ while (offset < endOffset) {
48
+ const header = await readAtomHeader(reader, offset);
49
+ if (!header || header.size <= 0)
50
+ break;
51
+ if (header.type === targetType) {
52
+ return {
53
+ dataOffset: offset + header.headerSize,
54
+ dataSize: header.size - header.headerSize,
55
+ };
56
+ }
57
+ offset += header.size;
58
+ }
59
+ return null;
60
+ }
61
+ /**
62
+ * Find the moov atom (contains all metadata)
63
+ */
64
+ async function findMoovAtom(reader) {
65
+ return findAtom(reader, 0, reader.size, 'moov');
66
+ }
67
+ /**
68
+ * Navigate to ilst (iTunes metadata list) atom
69
+ * Path: moov -> udta -> meta -> ilst
70
+ */
71
+ async function findIlstAtom(reader, moovOffset, moovSize) {
72
+ const udta = await findAtom(reader, moovOffset, moovOffset + moovSize, 'udta');
73
+ if (!udta)
74
+ return null;
75
+ const meta = await findAtom(reader, udta.dataOffset, udta.dataOffset + udta.dataSize, 'meta');
76
+ if (!meta)
77
+ return null;
78
+ // meta atom has 4 bytes of version/flags before child atoms
79
+ const metaDataStart = meta.dataOffset + 4;
80
+ const metaDataEnd = meta.dataOffset + meta.dataSize;
81
+ const ilst = await findAtom(reader, metaDataStart, metaDataEnd, 'ilst');
82
+ return ilst;
83
+ }
84
+ /**
85
+ * Read a text data atom value
86
+ */
87
+ async function readTextDataAtom(reader, atomOffset, atomSize) {
88
+ const data = await findAtom(reader, atomOffset, atomOffset + atomSize, 'data');
89
+ if (!data || data.dataSize < 8)
90
+ return undefined;
91
+ // data atom: 4 bytes type + 4 bytes locale + actual data
92
+ const content = await reader.read(data.dataOffset + 8, data.dataSize - 8);
93
+ return cleanText(content.toString('utf8'));
94
+ }
95
+ /**
96
+ * Read a numeric data atom value (for BPM, genre ID)
97
+ */
98
+ async function readNumericDataAtom(reader, atomOffset, atomSize) {
99
+ const data = await findAtom(reader, atomOffset, atomOffset + atomSize, 'data');
100
+ if (!data || data.dataSize < 10)
101
+ return undefined;
102
+ // data atom: 4 bytes type + 4 bytes locale + actual data
103
+ const content = await reader.read(data.dataOffset + 8, data.dataSize - 8);
104
+ // BPM is typically stored as 16-bit big-endian
105
+ if (content.length >= 2) {
106
+ return content.readUInt16BE(0);
107
+ }
108
+ return undefined;
109
+ }
110
+ /**
111
+ * Read cover artwork from covr atom
112
+ */
113
+ async function readCoverArtwork(reader, atomOffset, atomSize) {
114
+ const data = await findAtom(reader, atomOffset, atomOffset + atomSize, 'data');
115
+ if (!data || data.dataSize < 8)
116
+ return null;
117
+ const imageData = await reader.read(data.dataOffset + 8, data.dataSize - 8);
118
+ if (imageData.length === 0)
119
+ return null;
120
+ const mimeType = detectImageType(imageData) ?? 'image/jpeg';
121
+ return { data: imageData, mimeType };
122
+ }
123
+ /**
124
+ * Extract metadata from an MP4/M4A file
125
+ */
126
+ export async function extractFromMp4(reader) {
127
+ // Verify this is an MP4 file by checking for ftyp atom
128
+ const ftypHeader = await reader.read(0, 8);
129
+ if (ftypHeader.length < 8 || ftypHeader.toString('ascii', 4, 8) !== 'ftyp') {
130
+ return null;
131
+ }
132
+ // Find moov atom
133
+ const moov = await findMoovAtom(reader);
134
+ if (!moov)
135
+ return null;
136
+ // Find ilst atom (iTunes metadata)
137
+ const ilst = await findIlstAtom(reader, moov.dataOffset, moov.dataSize);
138
+ if (!ilst)
139
+ return null;
140
+ const metadata = {};
141
+ // Iterate through ilst child atoms
142
+ let offset = ilst.dataOffset;
143
+ const endOffset = ilst.dataOffset + ilst.dataSize;
144
+ while (offset < endOffset) {
145
+ const header = await readAtomHeader(reader, offset);
146
+ if (!header || header.size <= 0)
147
+ break;
148
+ const atomType = header.type;
149
+ const atomDataOffset = offset + header.headerSize;
150
+ const atomDataSize = header.size - header.headerSize;
151
+ // Extract metadata based on atom type
152
+ switch (atomType) {
153
+ case ATOM_TYPES.TITLE:
154
+ metadata.title = metadata.title ?? (await readTextDataAtom(reader, atomDataOffset, atomDataSize));
155
+ break;
156
+ case ATOM_TYPES.ARTIST:
157
+ metadata.artist = metadata.artist ?? (await readTextDataAtom(reader, atomDataOffset, atomDataSize));
158
+ break;
159
+ case ATOM_TYPES.ALBUM:
160
+ metadata.album = metadata.album ?? (await readTextDataAtom(reader, atomDataOffset, atomDataSize));
161
+ break;
162
+ case ATOM_TYPES.GENRE: {
163
+ const genre = await readTextDataAtom(reader, atomDataOffset, atomDataSize);
164
+ metadata.genre = metadata.genre ?? genre;
165
+ break;
166
+ }
167
+ case ATOM_TYPES.GENRE_ID: {
168
+ const genreId = await readNumericDataAtom(reader, atomDataOffset, atomDataSize);
169
+ if (genreId !== undefined && !metadata.genre) {
170
+ metadata.genre = ID3V1_GENRES[genreId - 1]; // MP4 genre IDs are 1-based
171
+ }
172
+ break;
173
+ }
174
+ case ATOM_TYPES.YEAR: {
175
+ const yearStr = await readTextDataAtom(reader, atomDataOffset, atomDataSize);
176
+ if (yearStr && !metadata.year) {
177
+ const match = yearStr.match(/\d{4}/);
178
+ if (match) {
179
+ metadata.year = parseInt(match[0], 10);
180
+ }
181
+ }
182
+ break;
183
+ }
184
+ case ATOM_TYPES.BPM: {
185
+ const bpm = await readNumericDataAtom(reader, atomDataOffset, atomDataSize);
186
+ if (bpm !== undefined && bpm > 0 && bpm < 500) {
187
+ metadata.bpm = metadata.bpm ?? bpm;
188
+ }
189
+ break;
190
+ }
191
+ case ATOM_TYPES.COVER: {
192
+ if (!metadata.artwork) {
193
+ const artwork = await readCoverArtwork(reader, atomDataOffset, atomDataSize);
194
+ if (artwork) {
195
+ metadata.artwork = artwork.data;
196
+ metadata.artworkMimeType = artwork.mimeType;
197
+ }
198
+ }
199
+ break;
200
+ }
201
+ }
202
+ offset += header.size;
203
+ }
204
+ // Return null if no metadata was found
205
+ if (Object.keys(metadata).length === 0) {
206
+ return null;
207
+ }
208
+ return metadata;
209
+ }
210
+ /**
211
+ * ID3v1 genre list (used for gnre atom)
212
+ */
213
+ const ID3V1_GENRES = {
214
+ 0: 'Blues',
215
+ 1: 'Classic Rock',
216
+ 2: 'Country',
217
+ 3: 'Dance',
218
+ 4: 'Disco',
219
+ 5: 'Funk',
220
+ 6: 'Grunge',
221
+ 7: 'Hip-Hop',
222
+ 8: 'Jazz',
223
+ 9: 'Metal',
224
+ 10: 'New Age',
225
+ 11: 'Oldies',
226
+ 12: 'Other',
227
+ 13: 'Pop',
228
+ 14: 'R&B',
229
+ 15: 'Rap',
230
+ 16: 'Reggae',
231
+ 17: 'Rock',
232
+ 18: 'Techno',
233
+ 19: 'Industrial',
234
+ 20: 'Alternative',
235
+ 21: 'Ska',
236
+ 22: 'Death Metal',
237
+ 23: 'Pranks',
238
+ 24: 'Soundtrack',
239
+ 25: 'Euro-Techno',
240
+ 26: 'Ambient',
241
+ 27: 'Trip-Hop',
242
+ 28: 'Vocal',
243
+ 29: 'Jazz+Funk',
244
+ 30: 'Fusion',
245
+ 31: 'Trance',
246
+ 32: 'Classical',
247
+ 33: 'Instrumental',
248
+ 34: 'Acid',
249
+ 35: 'House',
250
+ 36: 'Game',
251
+ 37: 'Sound Clip',
252
+ 38: 'Gospel',
253
+ 39: 'Noise',
254
+ 40: 'Alternative Rock',
255
+ 41: 'Bass',
256
+ 42: 'Soul',
257
+ 43: 'Punk',
258
+ 44: 'Space',
259
+ 45: 'Meditative',
260
+ 46: 'Instrumental Pop',
261
+ 47: 'Instrumental Rock',
262
+ 48: 'Ethnic',
263
+ 49: 'Gothic',
264
+ 50: 'Darkwave',
265
+ 51: 'Techno-Industrial',
266
+ 52: 'Electronic',
267
+ 53: 'Pop-Folk',
268
+ 54: 'Eurodance',
269
+ 55: 'Dream',
270
+ 56: 'Southern Rock',
271
+ 57: 'Comedy',
272
+ 58: 'Cult',
273
+ 59: 'Gangsta',
274
+ 60: 'Top 40',
275
+ 61: 'Christian Rap',
276
+ 62: 'Pop/Funk',
277
+ 63: 'Jungle',
278
+ 64: 'Native US',
279
+ 65: 'Cabaret',
280
+ 66: 'New Wave',
281
+ 67: 'Psychedelic',
282
+ 68: 'Rave',
283
+ 69: 'Showtunes',
284
+ 70: 'Trailer',
285
+ 71: 'Lo-Fi',
286
+ 72: 'Tribal',
287
+ 73: 'Acid Punk',
288
+ 74: 'Acid Jazz',
289
+ 75: 'Polka',
290
+ 76: 'Retro',
291
+ 77: 'Musical',
292
+ 78: 'Rock & Roll',
293
+ 79: 'Hard Rock',
294
+ };
295
+ //# sourceMappingURL=mp4.js.map