/
id3v2.ts
191 lines (176 loc) · 6.11 KB
/
id3v2.ts
1
2
3
4
5
6
7
8
9
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import fse from 'fs-extra';
import {Readable} from 'stream';
import {ID3v2Reader} from './id3v2.reader';
import {ID3v2Writer} from './id3v2.writer';
import {IID3V2} from './id3v2.types';
import {fileRangeToBuffer} from '../common/utils';
import {updateFile} from '../common/update-file';
import {ITagID} from '../common/types';
import {rawHeaderOffSet} from '../mp3/mp3.mpeg.frame';
import {checkID3v2} from './id3v2.check';
import {simplifyTag} from './id3v2.simplify';
import {FileWriterStream} from '../common/stream-writer-file';
import {writeRawFrames} from './frames/id3v2.frame.write';
import {buildID3v2} from './frames/id3v2.frame.read';
import {IMP3} from '../mp3/mp3.types';
/**
* Class for
* - reading ID3v2
* - writing ID3v2
* - removing ID3v2
*
* Basic usage example:
*
* ```ts
* [[include:snippet_id3v2-read.ts]]
* ```
*/
export class ID3v2 {
/**
* Checks an ID3v2 Tag for warnings
* @param tag the ID3v2 object to check
* @return a list returning warning messages
*/
static check(tag: IID3V2.Tag): Array<IID3V2.Warning> {
return checkID3v2(tag);
}
/**
* Checks an ID3v2 Tag for warnings
* @param tag the ID3v2 object to simplify
* @param dropIDsList a list of frame IDs to ignore, eg. 'APIC'
* @return a simplified ID3v2 object
*/
static simplify(tag: IID3V2.Tag, dropIDsList?: Array<string>): IID3V2.TagSimplified {
return simplifyTag(tag, dropIDsList);
}
/**
* Reads a filename & returns ID3v2 tag as Object
* @param filename the file to read
* @return a object returning i3v2 tag if found
*/
async read(filename: string): Promise<IID3V2.Tag | undefined> {
const reader = new ID3v2Reader();
const tag = await reader.read(filename);
if (tag) {
return await buildID3v2(tag);
}
}
/**
* Reads a stream & returns ID3v2 tag as Object
* @param stream the stream to read (NodeJS.stream.Readable)
* @return a object returning i3v2 tag if found
*/
async readStream(stream: Readable): Promise<IID3V2.Tag | undefined> {
const reader = new ID3v2Reader();
const tag = await reader.readStream(stream);
if (tag) {
return await buildID3v2(tag);
}
}
/**
* Reads a filename & returns ID3v2 tag as Buffer
* @param filename the file to read
* @return a object returning i3v2 tag if any found
*/
async readRaw(filename: string): Promise<Buffer | undefined> {
const reader = new ID3v2Reader();
const tag = await reader.read(filename);
if (tag) {
return await fileRangeToBuffer(filename, tag.start, tag.end);
}
}
/**
* Removes ID3v2 Tag from a file with given options
* @param filename the file to read
* @param options remove options
* @return true if tag has been found and removed
*/
async remove(filename: string, options: IID3V2.RemoveOptions): Promise<boolean> {
let removed = false;
await updateFile(filename, {id3v2: true, mpegQuick: true}, !!options.keepBackup, () => true, async (layout, fileWriter): Promise<void> => {
removed = await this.copyAudio(filename, layout, fileWriter);
});
return removed;
}
/**
* Writes ID3v2 Tag from a Builder object with given options
* @param filename the file to write
* @param options write options
*/
async writeBuilder(filename: string, builder: IID3V2.Builder, options: IID3V2.WriteOptions): Promise<void> {
await this.write(filename, {frames: builder.buildFrames()}, builder.version(), builder.rev(), options);
}
/**
* Writes ID3v2 Tag from an ID3v2 object with given options
* @param filename the file to write
* @param tag the ID3v2 object to write
* @param version the ID3v2.v version to write
* @param rev the ID3v2.v.r rev version to write
* @param options write options
*/
async write(filename: string, tag: IID3V2.ID3v2Tag, version: number, rev: number, options: IID3V2.WriteOptions): Promise<void> {
const opts = Object.assign({keepBackup: false, paddingSize: 100}, options);
const head = await this.buildHead(tag, version, rev);
const raw_frames = await writeRawFrames(tag.frames, head, options.defaultEncoding);
const exists = await fse.pathExists(filename);
if (!exists) {
await this.writeTag(filename, raw_frames, head);
} else {
await this.replaceTag(filename, raw_frames, head, opts);
}
}
private async buildHead(tag: IID3V2.ID3v2Tag, version: number, rev: number): Promise<IID3V2.TagHeader> {
const head: IID3V2.TagHeader = {ver: version, rev: rev, size: 0, valid: true, flagBits: tag.head ? tag.head.flagBits : undefined};
if (tag.head) {
if (version === 4 && tag.head.v4) {
head.v4 = tag.head.v4;
}
if (version === 3 && tag.head.v3) {
head.v3 = tag.head.v3;
}
if (version <= 2 && tag.head.v2) {
head.v2 = tag.head.v2;
}
}
return head;
}
private async writeTag(filename: string, frames: Array<IID3V2.RawFrame>, head: IID3V2.TagHeader): Promise<void> {
const stream = new FileWriterStream();
await stream.open(filename);
const writer = new ID3v2Writer();
try {
await writer.write(stream, frames, head, {paddingSize: 0});
} catch (e: any) {
await stream.close();
return Promise.reject(e);
}
await stream.close();
}
private async copyAudio(filename: string, layout: IMP3.RawLayout, fileWriter: FileWriterStream): Promise<boolean> {
let start = 0;
let specEnd = 0;
let skipped = false;
for (const tag of layout.tags) {
if ((tag.id === ITagID.ID3v2) && (start < tag.end)) {
specEnd = (tag as IID3V2.RawTag).head.size + tag.start + 10 /*header itself*/;
start = tag.end;
skipped = true;
}
}
if (layout.frameheaders.length > 0) {
const mediastart = rawHeaderOffSet(layout.frameheaders[0]);
start = specEnd < mediastart ? specEnd : mediastart;
} else {
start = Math.max(start, specEnd);
}
await fileWriter.copyFrom(filename, start);
return skipped;
}
private async replaceTag(filename: string, frames: Array<IID3V2.RawFrame>, head: IID3V2.TagHeader, options: IID3V2.WriteOptions): Promise<void> {
await updateFile(filename, {id3v2: true, mpegQuick: true}, !!options.keepBackup, () => true, async (layout, fileWriter): Promise<void> => {
const writer = new ID3v2Writer();
await writer.write(fileWriter, frames, head, options);
await this.copyAudio(filename, layout, fileWriter);
});
}
}