UNPKG

@catamphetamine/id3js

Version:

A modern ID3 parser written completely in JavaScript, making use of typed arrays and the HTML5 File API

134 lines (133 loc) 5.17 kB
import { parse as parseFrame } from './id3Frame.js'; import { getString, getUint32Synch, getUint24 } from './util.js'; import genres from './genres.js'; /** * Parses a given resource into an ID3 tag * @param {Reader} handle Reader to use for reading the resource * @return {Promise<ID3Tag|null>} */ export async function parse(handle) { let tag = null; /* * Read the last 128 bytes (ID3v1) */ const v1HeaderBuf = await handle.read(128, handle.size - 128); const v1Header = new DataView(v1HeaderBuf); if (v1HeaderBuf.byteLength === 128 && getString(v1Header, 3, undefined, true) === 'TAG') { tag = { kind: 'v1', title: getString(v1Header, 30, 3).trim() || null, album: getString(v1Header, 30, 63).trim() || null, artist: getString(v1Header, 30, 33).trim() || null, year: getString(v1Header, 4, 93).trim() || null, genre: null, comment: null, track: null }; /* * If there is a zero byte at [125], the comment is 28 bytes and the * remaining 2 are [0, trackno] */ if (v1Header.getUint8(125) === 0) { tag.comment = getString(v1Header, 28, 97); tag.version = 1.1; tag.track = v1Header.getUint8(126); } else { tag.comment = getString(v1Header, 30, 97); } /* * Lookup the genre index in the predefined genres array */ tag.genre = genres[v1Header.getUint8(127)] || null; } /* * Read 14 bytes (10 for ID3v2 header, 4 for possible extended header size) * Assuming the ID3v2 tag is prepended */ const v2PrefixBuf = await handle.read(14, 0); const v2Prefix = new DataView(v2PrefixBuf); /* * Be sure that the buffer is at least the size of an id3v2 header * Assume incompatibility if a major version of > 4 is used */ if (v2PrefixBuf.byteLength === 14 && getString(v2Prefix, 3, undefined, true) === 'ID3' && v2Prefix.getUint8(3) <= 4) { let headerSize = 10; let tagSize = 0; const version = [v2Prefix.getUint8(3), v2Prefix.getUint8(4)]; const tagFlags = v2Prefix.getUint8(5); /* * Do not support unsynchronisation */ if ((tagFlags & 0x80) === 0) { tag = { kind: 'v2', title: tag ? tag.title : null, album: tag ? tag.album : null, artist: tag ? tag.artist : null, year: tag ? tag.year : null, version: version, frames: [], images: [] }; /* * Increment the header size to offset by if an extended header exists */ if ((tagFlags & 0x40) !== 0) { headerSize += getUint32Synch(v2Prefix, 11); } /* * Calculate the tag size to be read */ tagSize += getUint32Synch(v2Prefix, 6); const v2TagBuf = await handle.read(tagSize, headerSize); const v2Tag = new DataView(v2TagBuf); let position = 0; while (position < v2TagBuf.byteLength) { let slice; let isFrame = true; for (let i = 0; i < 3; i++) { const frameBit = v2Tag.getUint8(position + i); if ((frameBit < 0x41 || frameBit > 0x5a) && (frameBit < 0x30 || frameBit > 0x39)) { isFrame = false; } } if (!isFrame) { break; } /* * < v2.3, frame ID is 3 chars, size is 3 bytes making a total * size of 6 bytes. * >= v2.3, frame ID is 4 chars, size is 4 bytes, flags are 2 bytes, * total 10 bytes. */ if (version[0] < 3) { slice = v2TagBuf.slice(position, position + 6 + getUint24(v2Tag, position + 3)); } else if (version[0] === 3) { slice = v2TagBuf.slice(position, position + 10 + v2Tag.getUint32(position + 4)); } else { slice = v2TagBuf.slice(position, position + 10 + getUint32Synch(v2Tag, position + 4)); } const frame = await parseFrame(slice, version[0], version[1]); if (frame && frame.tag) { const tagAsV2 = tag; tagAsV2.frames.push(frame); if (frame.tag === 'image') { tagAsV2.images.push(frame.value); } else { tag[frame.tag] = frame.value; } } position += slice.byteLength; } } } return tag; }