jamp3
Version:
mp3, id3v1, id3v2 - reader & writer
159 lines (147 loc) • 5.34 kB
text/typescript
import {bitarray, flags, removeZeroString, unsynchsafe} from '../common/utils';
import {BufferUtils} from '../common/buffer';
import {ID3v2_FRAME_FLAGS1, ID3v2_FRAME_FLAGS2, ID3v2_FRAME_HEADER, ID3v2_FRAME_HEADER_LENGTHS, ID3v2_MARKER} from './id3v2.header.consts';
import {IID3V2} from './id3v2.types';
import {Readable} from 'stream';
import {ITagID} from '../..';
import {ReaderStream} from '../common/stream-reader';
import {BufferReader} from '../common/buffer-reader';
import {isValidFrameBinId} from './frames/id3v2.frame.match';
import {ID3v2HeaderReader} from './id3v2.reader.header';
const ID3v2_MARKER_BUFFER = BufferUtils.fromString(ID3v2_MARKER);
export class ID3v2Reader {
headerReader = new ID3v2HeaderReader();
private async readRawTag(head: IID3V2.TagHeader, reader: ReaderStream): Promise<{ rest?: Buffer; tag?: IID3V2.RawTag }> {
const tag: IID3V2.RawTag = {id: ITagID.ID3v2, frames: [], start: 0, end: 0, head: head || {ver: 0, rev: 0, size: 0, valid: false}};
let rest: Buffer | undefined;
if (tag.head.size > 0) {
const data = await reader.read(tag.head.size);
rest = await this.readFrames(data, tag);
}
return {rest, tag};
}
private async scan(reader: ReaderStream): Promise<{ rest?: Buffer; tag?: IID3V2.RawTag }> {
if (reader.end) {
return {};
}
const index = await reader.scan(ID3v2_MARKER_BUFFER);
if (index < 0) {
return {};
}
const result = await this.readReaderStream(reader);
if (!result.tag) {
return this.scan(reader);
}
result.tag.start = index;
result.tag.end = reader.pos;
return result;
}
private async scanReaderStream(reader: ReaderStream): Promise<IID3V2.RawTag | undefined> {
const result = await this.scan(reader);
return result.tag;
}
public async readReaderStream(reader: ReaderStream): Promise<{ rest?: Buffer; tag?: IID3V2.RawTag }> {
const head = await this.headerReader.readHeader(reader);
if (!head) {
return {};
}
if (!head.header) {
return {rest: head.rest};
}
return await this.readRawTag(head.header, reader);
}
public async readStream(stream: Readable): Promise<IID3V2.RawTag | undefined> {
const reader = new ReaderStream();
try {
await reader.openStream(stream);
return await this.scanReaderStream(reader);
} catch (e: any) {
return Promise.reject(e);
}
}
public async read(filename: string): Promise<IID3V2.RawTag | undefined> {
const reader = new ReaderStream();
try {
await reader.open(filename);
const tag = await this.scanReaderStream(reader);
reader.close();
return tag;
} catch (e: any) {
reader.close();
return Promise.reject(e);
}
}
public async readFrames(data: Buffer, tag: IID3V2.RawTag): Promise<Buffer> {
const markerLength = ID3v2_FRAME_HEADER_LENGTHS.MARKER[tag.head.ver];
const reader = new BufferReader(data);
let finish = false;
let skip = 0;
while (!finish) {
let idbin = reader.readBuffer(markerLength);
if (idbin[0] === 0) { // found a zero byte, game over
reader.position -= markerLength;
return reader.rest();
}
while (reader.hasData() && (!isValidFrameBinId(idbin))) {
reader.position -= (markerLength - 1);
skip++;
idbin = reader.readBuffer(markerLength);
}
if (reader.hasData() && isValidFrameBinId(idbin)) {
if (reader.unread() < ID3v2_FRAME_HEADER_LENGTHS.SIZE[tag.head.ver]) {
return reader.rest();
}
skip = this.readFrame(reader, idbin, tag, skip);
} else {
finish = true;
}
}
if (skip > 0) {
reader.position -= (skip + markerLength);
}
return reader.rest();
}
private defaultRawFrame(idbin: Buffer, tag: IID3V2.RawTag): IID3V2.RawFrame {
return {
id: removeZeroString(idbin.toString('ascii').trim()),
size: 0, start: tag.start, end: tag.end,
data: BufferUtils.zeroBuffer(0),
statusFlags: {}, formatFlags: {}
};
}
private readFrame(reader: BufferReader, idbin: Buffer, tag: IID3V2.RawTag, skip: number): number {
const markerLength = ID3v2_FRAME_HEADER_LENGTHS.MARKER[tag.head.ver];
const pos = reader.position;
const frame: IID3V2.RawFrame = this.defaultRawFrame(idbin, tag);
frame.size = reader.readUInt(ID3v2_FRAME_HEADER_LENGTHS.SIZE[tag.head.ver]);
if (ID3v2_FRAME_HEADER.SYNCSAVEINT.includes(tag.head.ver)) {
frame.size = unsynchsafe(frame.size);
}
if (ID3v2_FRAME_HEADER_LENGTHS.FLAGS[tag.head.ver] > 0) {
frame.statusFlags = flags(ID3v2_FRAME_FLAGS1[tag.head.ver], bitarray(reader.readByte()));
frame.formatFlags = flags(ID3v2_FRAME_FLAGS2[tag.head.ver], bitarray(reader.readByte()));
}
let valid = (!frame.statusFlags.reserved && !frame.formatFlags.reserved2 && !frame.formatFlags.reserved3);
if (valid && frame.size > reader.unread()) {
valid = false;
}
if (valid) {
if (skip > 0 && tag.frames.length > 0) {
const lastFrame = tag.frames[tag.frames.length - 1];
lastFrame.data = BufferUtils.concatBuffer(lastFrame.data, reader.data.slice(pos - skip - markerLength, pos - markerLength));
lastFrame.size = lastFrame.data.length;
}
skip = 0;
if (frame.size > 0) {
frame.data = (tag.head.v3 && tag.head.v3.flags.unsynchronisation) ?
reader.readUnsyncedBuffer(frame.size) :
reader.readBuffer(frame.size);
}
tag.frames.push(frame);
} else {
reader.position = pos - (markerLength - 1);
skip++;
}
return skip;
}
}