taglib-wasm 0.2.4 → 0.2.6
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.
- package/README.md +81 -14
- package/build/taglib.js +1 -16
- package/build/taglib.wasm +0 -0
- package/index.ts +1 -1
- package/package.json +4 -2
- package/src/enhanced-api.ts +1 -1
- package/src/simple-jsr.ts +201 -0
- package/src/simple.ts +313 -0
- package/src/taglib-embind.ts +231 -0
- package/src/taglib-jsr.ts +152 -2
- package/src/taglib.ts +150 -1
- package/src/types.ts +30 -1
- package/src/wasm-embind.ts +55 -0
- package/src/wasm-jsr.ts +156 -55
- package/src/wasm-workers.ts +4 -4
- package/src/wasm.ts +17 -5
package/src/simple.ts
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Simplified API for taglib-wasm matching go-taglib's interface
|
|
3
|
+
*
|
|
4
|
+
* This module provides a dead-simple API for reading and writing audio metadata,
|
|
5
|
+
* inspired by go-taglib's excellent developer experience.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { readTags, writeTags, readProperties } from "taglib-wasm/simple";
|
|
10
|
+
*
|
|
11
|
+
* // Read tags
|
|
12
|
+
* const tags = await readTags("song.mp3");
|
|
13
|
+
* console.log(tags.album);
|
|
14
|
+
*
|
|
15
|
+
* // Write tags
|
|
16
|
+
* await writeTags("song.mp3", {
|
|
17
|
+
* album: "New Album",
|
|
18
|
+
* artist: "New Artist"
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Read audio properties
|
|
22
|
+
* const props = await readProperties("song.mp3");
|
|
23
|
+
* console.log(`Duration: ${props.length}s, Bitrate: ${props.bitrate}kbps`);
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { TagLib } from "./taglib.ts";
|
|
28
|
+
import type { AudioProperties, Tag } from "./types.ts";
|
|
29
|
+
|
|
30
|
+
// Cached TagLib instance for auto-initialization
|
|
31
|
+
let cachedTagLib: TagLib | null = null;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get or create a TagLib instance with auto-initialization
|
|
35
|
+
*/
|
|
36
|
+
async function getTagLib(): Promise<TagLib> {
|
|
37
|
+
if (!cachedTagLib) {
|
|
38
|
+
cachedTagLib = await TagLib.initialize({
|
|
39
|
+
debug: false,
|
|
40
|
+
memory: {
|
|
41
|
+
initial: 16 * 1024 * 1024, // 16MB default
|
|
42
|
+
maximum: 64 * 1024 * 1024, // 64MB max
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return cachedTagLib;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read a file's data from various sources
|
|
51
|
+
*/
|
|
52
|
+
async function readFileData(file: string | Uint8Array | ArrayBuffer | File): Promise<Uint8Array> {
|
|
53
|
+
// Already a Uint8Array
|
|
54
|
+
if (file instanceof Uint8Array) {
|
|
55
|
+
return file;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ArrayBuffer - convert to Uint8Array
|
|
59
|
+
if (file instanceof ArrayBuffer) {
|
|
60
|
+
return new Uint8Array(file);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// File object (browser)
|
|
64
|
+
if (typeof File !== 'undefined' && file instanceof File) {
|
|
65
|
+
return new Uint8Array(await file.arrayBuffer());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// String path - read from filesystem
|
|
69
|
+
if (typeof file === 'string') {
|
|
70
|
+
// Deno
|
|
71
|
+
if (typeof Deno !== 'undefined') {
|
|
72
|
+
return await Deno.readFile(file);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Node.js
|
|
76
|
+
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
|
|
77
|
+
const { readFile } = await import('fs/promises');
|
|
78
|
+
return new Uint8Array(await readFile(file));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Bun
|
|
82
|
+
if (typeof (globalThis as any).Bun !== 'undefined') {
|
|
83
|
+
const bunFile = (globalThis as any).Bun.file(file);
|
|
84
|
+
return new Uint8Array(await bunFile.arrayBuffer());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw new Error('File path reading not supported in this environment');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
throw new Error('Invalid file input type');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Read metadata tags from an audio file
|
|
95
|
+
*
|
|
96
|
+
* @param file - File path, Uint8Array buffer, ArrayBuffer, or File object
|
|
97
|
+
* @returns Object containing all metadata tags
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const tags = await readTags("song.mp3");
|
|
102
|
+
* console.log(tags.title, tags.artist, tags.album);
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export async function readTags(file: string | Uint8Array | ArrayBuffer | File): Promise<Tag> {
|
|
106
|
+
const taglib = await getTagLib();
|
|
107
|
+
const audioData = await readFileData(file);
|
|
108
|
+
|
|
109
|
+
const audioFile = taglib.openFile(audioData);
|
|
110
|
+
try {
|
|
111
|
+
if (!audioFile.isValid()) {
|
|
112
|
+
throw new Error('Invalid audio file');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return audioFile.tag();
|
|
116
|
+
} finally {
|
|
117
|
+
audioFile.dispose();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Write metadata tags to an audio file
|
|
123
|
+
*
|
|
124
|
+
* Note: This modifies the in-memory representation only.
|
|
125
|
+
* To persist changes, you need to get the modified buffer.
|
|
126
|
+
*
|
|
127
|
+
* @param file - File path, Uint8Array buffer, ArrayBuffer, or File object
|
|
128
|
+
* @param tags - Object containing tags to write (undefined values are ignored)
|
|
129
|
+
* @param options - Write options (currently unused, for go-taglib compatibility)
|
|
130
|
+
* @returns Modified file buffer
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const modifiedBuffer = await writeTags("song.mp3", {
|
|
135
|
+
* title: "New Title",
|
|
136
|
+
* artist: "New Artist",
|
|
137
|
+
* album: "New Album",
|
|
138
|
+
* year: 2025
|
|
139
|
+
* });
|
|
140
|
+
* // Save modifiedBuffer to file or use as needed
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export async function writeTags(
|
|
144
|
+
file: string | Uint8Array | ArrayBuffer | File,
|
|
145
|
+
tags: Partial<Tag>,
|
|
146
|
+
options?: number
|
|
147
|
+
): Promise<Uint8Array> {
|
|
148
|
+
const taglib = await getTagLib();
|
|
149
|
+
const audioData = await readFileData(file);
|
|
150
|
+
|
|
151
|
+
const audioFile = taglib.openFile(audioData);
|
|
152
|
+
try {
|
|
153
|
+
if (!audioFile.isValid()) {
|
|
154
|
+
throw new Error('Invalid audio file');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Write each tag if defined
|
|
158
|
+
if (tags.title !== undefined) audioFile.setTitle(tags.title);
|
|
159
|
+
if (tags.artist !== undefined) audioFile.setArtist(tags.artist);
|
|
160
|
+
if (tags.album !== undefined) audioFile.setAlbum(tags.album);
|
|
161
|
+
if (tags.comment !== undefined) audioFile.setComment(tags.comment);
|
|
162
|
+
if (tags.genre !== undefined) audioFile.setGenre(tags.genre);
|
|
163
|
+
if (tags.year !== undefined) audioFile.setYear(tags.year);
|
|
164
|
+
if (tags.track !== undefined) audioFile.setTrack(tags.track);
|
|
165
|
+
|
|
166
|
+
// Save changes to in-memory buffer
|
|
167
|
+
if (!audioFile.save()) {
|
|
168
|
+
throw new Error('Failed to save changes');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Note: In a real implementation, we'd need to get the modified buffer
|
|
172
|
+
// For now, return the original as taglib-wasm doesn't expose the modified buffer yet
|
|
173
|
+
return audioData;
|
|
174
|
+
} finally {
|
|
175
|
+
audioFile.dispose();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Read audio properties from a file
|
|
181
|
+
*
|
|
182
|
+
* @param file - File path, Uint8Array buffer, ArrayBuffer, or File object
|
|
183
|
+
* @returns Audio properties including duration, bitrate, sample rate, etc.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* const props = await readProperties("song.mp3");
|
|
188
|
+
* console.log(`Duration: ${props.length} seconds`);
|
|
189
|
+
* console.log(`Bitrate: ${props.bitrate} kbps`);
|
|
190
|
+
* console.log(`Sample rate: ${props.sampleRate} Hz`);
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
export async function readProperties(file: string | Uint8Array | ArrayBuffer | File): Promise<AudioProperties> {
|
|
194
|
+
const taglib = await getTagLib();
|
|
195
|
+
const audioData = await readFileData(file);
|
|
196
|
+
|
|
197
|
+
const audioFile = taglib.openFile(audioData);
|
|
198
|
+
try {
|
|
199
|
+
if (!audioFile.isValid()) {
|
|
200
|
+
throw new Error('Invalid audio file');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const props = audioFile.audioProperties();
|
|
204
|
+
if (!props) {
|
|
205
|
+
throw new Error('Failed to read audio properties');
|
|
206
|
+
}
|
|
207
|
+
return props;
|
|
208
|
+
} finally {
|
|
209
|
+
audioFile.dispose();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Tag field constants for go-taglib compatibility
|
|
215
|
+
* These match the constants used in go-taglib for consistent API
|
|
216
|
+
*/
|
|
217
|
+
export const Title = "title";
|
|
218
|
+
export const Artist = "artist";
|
|
219
|
+
export const Album = "album";
|
|
220
|
+
export const Comment = "comment";
|
|
221
|
+
export const Genre = "genre";
|
|
222
|
+
export const Year = "year";
|
|
223
|
+
export const Track = "track";
|
|
224
|
+
export const AlbumArtist = "albumartist";
|
|
225
|
+
export const Composer = "composer";
|
|
226
|
+
export const DiscNumber = "discnumber";
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Additional convenience functions
|
|
230
|
+
*/
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check if a file is a valid audio file
|
|
234
|
+
*
|
|
235
|
+
* @param file - File path, Uint8Array buffer, ArrayBuffer, or File object
|
|
236
|
+
* @returns true if the file is a valid audio file
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```typescript
|
|
240
|
+
* if (await isValidAudioFile("maybe-audio.bin")) {
|
|
241
|
+
* const tags = await readTags("maybe-audio.bin");
|
|
242
|
+
* }
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
export async function isValidAudioFile(file: string | Uint8Array | ArrayBuffer | File): Promise<boolean> {
|
|
246
|
+
try {
|
|
247
|
+
const taglib = await getTagLib();
|
|
248
|
+
const audioData = await readFileData(file);
|
|
249
|
+
|
|
250
|
+
const audioFile = taglib.openFile(audioData);
|
|
251
|
+
const valid = audioFile.isValid();
|
|
252
|
+
audioFile.dispose();
|
|
253
|
+
|
|
254
|
+
return valid;
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get the audio format of a file
|
|
262
|
+
*
|
|
263
|
+
* @param file - File path, Uint8Array buffer, ArrayBuffer, or File object
|
|
264
|
+
* @returns Audio format string (e.g., "MP3", "FLAC", "OGG") or undefined
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```typescript
|
|
268
|
+
* const format = await getFormat("song.mp3");
|
|
269
|
+
* console.log(`File format: ${format}`); // "MP3"
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
export async function getFormat(file: string | Uint8Array | ArrayBuffer | File): Promise<string | undefined> {
|
|
273
|
+
const taglib = await getTagLib();
|
|
274
|
+
const audioData = await readFileData(file);
|
|
275
|
+
|
|
276
|
+
const audioFile = taglib.openFile(audioData);
|
|
277
|
+
try {
|
|
278
|
+
if (!audioFile.isValid()) {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return audioFile.format();
|
|
283
|
+
} finally {
|
|
284
|
+
audioFile.dispose();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Clear all tags from a file
|
|
290
|
+
*
|
|
291
|
+
* @param file - File path, Uint8Array buffer, ArrayBuffer, or File object
|
|
292
|
+
* @returns Modified file buffer with tags removed
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* ```typescript
|
|
296
|
+
* const cleanBuffer = await clearTags("song.mp3");
|
|
297
|
+
* // Save cleanBuffer to remove all metadata
|
|
298
|
+
* ```
|
|
299
|
+
*/
|
|
300
|
+
export async function clearTags(file: string | Uint8Array | ArrayBuffer | File): Promise<Uint8Array> {
|
|
301
|
+
return writeTags(file, {
|
|
302
|
+
title: "",
|
|
303
|
+
artist: "",
|
|
304
|
+
album: "",
|
|
305
|
+
comment: "",
|
|
306
|
+
genre: "",
|
|
307
|
+
year: 0,
|
|
308
|
+
track: 0,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Type exports for convenience
|
|
313
|
+
export type { Tag, AudioProperties } from "./types.ts";
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { TagLibModule, WasmModule } from "./wasm-embind.ts";
|
|
2
|
+
import type { AudioFile as AudioFileInterface, AudioProperties, FileType, PropertyMap, Tag } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Audio file wrapper using Embind API
|
|
6
|
+
*/
|
|
7
|
+
export class AudioFile implements AudioFileInterface {
|
|
8
|
+
private fileHandle: any;
|
|
9
|
+
private cachedTag: Tag | null = null;
|
|
10
|
+
private cachedAudioProperties: AudioProperties | null = null;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
private module: TagLibModule,
|
|
14
|
+
fileHandle: any,
|
|
15
|
+
) {
|
|
16
|
+
this.fileHandle = fileHandle;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get file format
|
|
21
|
+
*/
|
|
22
|
+
getFormat(): FileType {
|
|
23
|
+
return this.fileHandle.getFormat() as FileType;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get tag object for reading/writing metadata
|
|
28
|
+
*/
|
|
29
|
+
tag(): Tag {
|
|
30
|
+
if (!this.cachedTag) {
|
|
31
|
+
const tagWrapper = this.fileHandle.getTag();
|
|
32
|
+
if (!tagWrapper) {
|
|
33
|
+
throw new Error("Failed to get tag from file");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.cachedTag = {
|
|
37
|
+
title: () => tagWrapper.title(),
|
|
38
|
+
artist: () => tagWrapper.artist(),
|
|
39
|
+
album: () => tagWrapper.album(),
|
|
40
|
+
comment: () => tagWrapper.comment(),
|
|
41
|
+
genre: () => tagWrapper.genre(),
|
|
42
|
+
year: () => tagWrapper.year(),
|
|
43
|
+
track: () => tagWrapper.track(),
|
|
44
|
+
|
|
45
|
+
setTitle: (value: string) => tagWrapper.setTitle(value),
|
|
46
|
+
setArtist: (value: string) => tagWrapper.setArtist(value),
|
|
47
|
+
setAlbum: (value: string) => tagWrapper.setAlbum(value),
|
|
48
|
+
setComment: (value: string) => tagWrapper.setComment(value),
|
|
49
|
+
setGenre: (value: string) => tagWrapper.setGenre(value),
|
|
50
|
+
setYear: (value: number) => tagWrapper.setYear(value),
|
|
51
|
+
setTrack: (value: number) => tagWrapper.setTrack(value),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return this.cachedTag;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get audio properties
|
|
60
|
+
*/
|
|
61
|
+
audioProperties(): AudioProperties | null {
|
|
62
|
+
if (!this.cachedAudioProperties) {
|
|
63
|
+
const propsWrapper = this.fileHandle.getAudioProperties();
|
|
64
|
+
if (!propsWrapper) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.cachedAudioProperties = {
|
|
69
|
+
length: propsWrapper.lengthInSeconds(),
|
|
70
|
+
lengthInMilliseconds: propsWrapper.lengthInMilliseconds(),
|
|
71
|
+
bitrate: propsWrapper.bitrate(),
|
|
72
|
+
sampleRate: propsWrapper.sampleRate(),
|
|
73
|
+
channels: propsWrapper.channels(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return this.cachedAudioProperties;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get all properties as a PropertyMap
|
|
82
|
+
*/
|
|
83
|
+
properties(): PropertyMap {
|
|
84
|
+
const jsObj = this.fileHandle.getProperties();
|
|
85
|
+
const result: PropertyMap = {};
|
|
86
|
+
|
|
87
|
+
// Convert from Emscripten val to plain object
|
|
88
|
+
const keys = Object.keys(jsObj);
|
|
89
|
+
for (const key of keys) {
|
|
90
|
+
result[key] = jsObj[key];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Set properties from a PropertyMap
|
|
98
|
+
*/
|
|
99
|
+
setProperties(properties: PropertyMap): void {
|
|
100
|
+
this.fileHandle.setProperties(properties);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get a single property value
|
|
105
|
+
*/
|
|
106
|
+
getProperty(key: string): string | undefined {
|
|
107
|
+
const value = this.fileHandle.getProperty(key);
|
|
108
|
+
return value === "" ? undefined : value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Set a single property value
|
|
113
|
+
*/
|
|
114
|
+
setProperty(key: string, value: string): void {
|
|
115
|
+
this.fileHandle.setProperty(key, value);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if this is an MP4 file
|
|
120
|
+
*/
|
|
121
|
+
isMP4(): boolean {
|
|
122
|
+
return this.fileHandle.isMP4();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get MP4-specific item
|
|
127
|
+
*/
|
|
128
|
+
getMP4Item(key: string): string | undefined {
|
|
129
|
+
if (!this.isMP4()) {
|
|
130
|
+
throw new Error("Not an MP4 file");
|
|
131
|
+
}
|
|
132
|
+
const value = this.fileHandle.getMP4Item(key);
|
|
133
|
+
return value === "" ? undefined : value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Set MP4-specific item
|
|
138
|
+
*/
|
|
139
|
+
setMP4Item(key: string, value: string): void {
|
|
140
|
+
if (!this.isMP4()) {
|
|
141
|
+
throw new Error("Not an MP4 file");
|
|
142
|
+
}
|
|
143
|
+
this.fileHandle.setMP4Item(key, value);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Remove MP4-specific item
|
|
148
|
+
*/
|
|
149
|
+
removeMP4Item(key: string): void {
|
|
150
|
+
if (!this.isMP4()) {
|
|
151
|
+
throw new Error("Not an MP4 file");
|
|
152
|
+
}
|
|
153
|
+
this.fileHandle.removeMP4Item(key);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Save changes to the file
|
|
158
|
+
*/
|
|
159
|
+
save(): boolean {
|
|
160
|
+
// Clear caches since values may have changed
|
|
161
|
+
this.cachedTag = null;
|
|
162
|
+
this.cachedAudioProperties = null;
|
|
163
|
+
|
|
164
|
+
return this.fileHandle.save();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check if the file is valid
|
|
169
|
+
*/
|
|
170
|
+
isValid(): boolean {
|
|
171
|
+
return this.fileHandle.isValid();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Free resources
|
|
176
|
+
*/
|
|
177
|
+
dispose(): void {
|
|
178
|
+
if (this.fileHandle) {
|
|
179
|
+
// Embind will handle cleanup when the object goes out of scope
|
|
180
|
+
// But we can help by clearing our references
|
|
181
|
+
this.fileHandle = null;
|
|
182
|
+
this.cachedTag = null;
|
|
183
|
+
this.cachedAudioProperties = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Main TagLib interface using Embind
|
|
190
|
+
*/
|
|
191
|
+
export class TagLib {
|
|
192
|
+
private module: TagLibModule;
|
|
193
|
+
|
|
194
|
+
constructor(module: WasmModule) {
|
|
195
|
+
this.module = module as TagLibModule;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Open a file from a buffer
|
|
200
|
+
*/
|
|
201
|
+
async openFile(buffer: ArrayBuffer): Promise<AudioFile> {
|
|
202
|
+
// Convert ArrayBuffer to string for Embind
|
|
203
|
+
const uint8Array = new Uint8Array(buffer);
|
|
204
|
+
const binaryString = Array.from(uint8Array, byte => String.fromCharCode(byte)).join('');
|
|
205
|
+
|
|
206
|
+
// Create a new FileHandle
|
|
207
|
+
const fileHandle = this.module.createFileHandle();
|
|
208
|
+
|
|
209
|
+
// Load the buffer
|
|
210
|
+
const success = fileHandle.loadFromBuffer(binaryString);
|
|
211
|
+
if (!success) {
|
|
212
|
+
throw new Error("Failed to load file from buffer");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return new AudioFile(this.module, fileHandle);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get version information
|
|
220
|
+
*/
|
|
221
|
+
version(): string {
|
|
222
|
+
return "2.1.0"; // TagLib version we're using
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Create a TagLib instance
|
|
228
|
+
*/
|
|
229
|
+
export async function createTagLib(module: WasmModule): Promise<TagLib> {
|
|
230
|
+
return new TagLib(module);
|
|
231
|
+
}
|
package/src/taglib-jsr.ts
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
import type {
|
|
9
9
|
AudioFormat,
|
|
10
10
|
AudioProperties,
|
|
11
|
+
BitrateControlMode,
|
|
11
12
|
ExtendedTag,
|
|
12
13
|
Picture,
|
|
14
|
+
PropertyMap,
|
|
13
15
|
Tag,
|
|
14
16
|
TagLibConfig,
|
|
15
17
|
} from "./types.ts";
|
|
@@ -19,7 +21,11 @@ import {
|
|
|
19
21
|
loadTagLibModuleJSR,
|
|
20
22
|
type TagLibModule,
|
|
21
23
|
} from "./wasm-jsr.ts";
|
|
22
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
BITRATE_CONTROL_MODE_NAMES,
|
|
26
|
+
BITRATE_CONTROL_MODE_VALUES,
|
|
27
|
+
METADATA_MAPPINGS
|
|
28
|
+
} from "./types.ts";
|
|
23
29
|
|
|
24
30
|
/**
|
|
25
31
|
* JSR-compatible TagLib singleton for WASM module management
|
|
@@ -59,7 +65,7 @@ export class TagLibJSR {
|
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
/**
|
|
62
|
-
* Initialize
|
|
68
|
+
* Initialize taglib-wasm module
|
|
63
69
|
*/
|
|
64
70
|
static async initialize(config?: TagLibConfig): Promise<void> {
|
|
65
71
|
const instance = TagLibJSR.getInstance();
|
|
@@ -358,6 +364,150 @@ export class AudioFileJSR {
|
|
|
358
364
|
}
|
|
359
365
|
setAppleSoundCheck(soundCheck: string): void {}
|
|
360
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Get all properties as a PropertyMap
|
|
369
|
+
*/
|
|
370
|
+
properties(): PropertyMap {
|
|
371
|
+
const jsonPtr = this.module._taglib_file_properties_json(this.fileId);
|
|
372
|
+
if (jsonPtr === 0) return {};
|
|
373
|
+
|
|
374
|
+
const jsonStr = cStringToJSJSR(this.module, jsonPtr);
|
|
375
|
+
try {
|
|
376
|
+
return JSON.parse(jsonStr);
|
|
377
|
+
} catch {
|
|
378
|
+
return {};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Set properties from a PropertyMap
|
|
384
|
+
*/
|
|
385
|
+
setProperties(properties: PropertyMap): boolean {
|
|
386
|
+
const jsonStr = JSON.stringify(properties);
|
|
387
|
+
const jsonPtr = jsToCStringJSR(this.module, jsonStr);
|
|
388
|
+
try {
|
|
389
|
+
return this.module._taglib_file_set_properties_json(this.fileId, jsonPtr) !== 0;
|
|
390
|
+
} finally {
|
|
391
|
+
this.module._free(jsonPtr);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Get a specific property value
|
|
397
|
+
*/
|
|
398
|
+
getProperty(key: string): string[] | undefined {
|
|
399
|
+
const keyPtr = jsToCStringJSR(this.module, key);
|
|
400
|
+
try {
|
|
401
|
+
const valuePtr = this.module._taglib_file_get_property(this.fileId, keyPtr);
|
|
402
|
+
if (valuePtr === 0) return undefined;
|
|
403
|
+
|
|
404
|
+
const value = cStringToJSJSR(this.module, valuePtr);
|
|
405
|
+
return value ? [value] : undefined;
|
|
406
|
+
} finally {
|
|
407
|
+
this.module._free(keyPtr);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Set a specific property value
|
|
413
|
+
*/
|
|
414
|
+
setProperty(key: string, values: string | string[]): boolean {
|
|
415
|
+
const value = Array.isArray(values) ? values[0] : values;
|
|
416
|
+
if (!value) return false;
|
|
417
|
+
|
|
418
|
+
const keyPtr = jsToCStringJSR(this.module, key);
|
|
419
|
+
const valuePtr = jsToCStringJSR(this.module, value);
|
|
420
|
+
try {
|
|
421
|
+
return this.module._taglib_file_set_property(this.fileId, keyPtr, valuePtr) !== 0;
|
|
422
|
+
} finally {
|
|
423
|
+
this.module._free(keyPtr);
|
|
424
|
+
this.module._free(valuePtr);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Check if this is an MP4 file
|
|
430
|
+
*/
|
|
431
|
+
isMP4(): boolean {
|
|
432
|
+
return this.module._taglib_file_is_mp4(this.fileId) !== 0;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Get MP4-specific item (for custom atoms)
|
|
437
|
+
*/
|
|
438
|
+
getMP4Item(key: string): string | undefined {
|
|
439
|
+
if (!this.isMP4()) return undefined;
|
|
440
|
+
|
|
441
|
+
const keyPtr = jsToCStringJSR(this.module, key);
|
|
442
|
+
try {
|
|
443
|
+
const valuePtr = this.module._taglib_mp4_get_item(this.fileId, keyPtr);
|
|
444
|
+
if (valuePtr === 0) return undefined;
|
|
445
|
+
|
|
446
|
+
return cStringToJSJSR(this.module, valuePtr);
|
|
447
|
+
} finally {
|
|
448
|
+
this.module._free(keyPtr);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Set MP4-specific item (for custom atoms)
|
|
454
|
+
*/
|
|
455
|
+
setMP4Item(key: string, value: string): boolean {
|
|
456
|
+
if (!this.isMP4()) return false;
|
|
457
|
+
|
|
458
|
+
const keyPtr = jsToCStringJSR(this.module, key);
|
|
459
|
+
const valuePtr = jsToCStringJSR(this.module, value);
|
|
460
|
+
try {
|
|
461
|
+
return this.module._taglib_mp4_set_item(this.fileId, keyPtr, valuePtr) !== 0;
|
|
462
|
+
} finally {
|
|
463
|
+
this.module._free(keyPtr);
|
|
464
|
+
this.module._free(valuePtr);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Remove MP4-specific item
|
|
470
|
+
*/
|
|
471
|
+
removeMP4Item(key: string): boolean {
|
|
472
|
+
if (!this.isMP4()) return false;
|
|
473
|
+
|
|
474
|
+
const keyPtr = jsToCStringJSR(this.module, key);
|
|
475
|
+
try {
|
|
476
|
+
return this.module._taglib_mp4_remove_item(this.fileId, keyPtr) !== 0;
|
|
477
|
+
} finally {
|
|
478
|
+
this.module._free(keyPtr);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Get bitrate control mode (MP4/M4A specific)
|
|
484
|
+
* Reads from the 'acbf' atom
|
|
485
|
+
*/
|
|
486
|
+
getBitrateControlMode(): BitrateControlMode | undefined {
|
|
487
|
+
if (!this.isMP4()) return undefined;
|
|
488
|
+
|
|
489
|
+
const value = this.getMP4Item("acbf");
|
|
490
|
+
if (!value) return undefined;
|
|
491
|
+
|
|
492
|
+
const numValue = parseInt(value, 10);
|
|
493
|
+
if (isNaN(numValue) || numValue < 0 || numValue > 3) return undefined;
|
|
494
|
+
|
|
495
|
+
return BITRATE_CONTROL_MODE_NAMES[numValue];
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Set bitrate control mode (MP4/M4A specific)
|
|
500
|
+
* Writes to the 'acbf' atom
|
|
501
|
+
*/
|
|
502
|
+
setBitrateControlMode(mode: BitrateControlMode): boolean {
|
|
503
|
+
if (!this.isMP4()) return false;
|
|
504
|
+
|
|
505
|
+
const numValue = BITRATE_CONTROL_MODE_VALUES[mode];
|
|
506
|
+
if (numValue === undefined) return false;
|
|
507
|
+
|
|
508
|
+
return this.setMP4Item("acbf", numValue.toString());
|
|
509
|
+
}
|
|
510
|
+
|
|
361
511
|
/**
|
|
362
512
|
* Save changes to the file
|
|
363
513
|
*/
|