taglib-wasm 0.2.7 → 0.3.1
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 +155 -149
- package/build/taglib.js +4 -10
- package/build/taglib.wasm +0 -0
- package/index.ts +17 -3
- package/package.json +9 -9
- package/src/mod.ts +14 -3
- package/src/simple.ts +21 -25
- package/src/taglib.ts +179 -496
- package/src/types.ts +6 -3
- package/src/wasm-workers.ts +9 -1
- package/src/wasm.ts +105 -244
- package/src/workers.ts +50 -29
- package/src/enhanced-api.ts +0 -296
- package/src/simple-jsr.ts +0 -201
- package/src/taglib-embind.ts +0 -231
- package/src/taglib-jsr.ts +0 -544
- package/src/wasm-embind.ts +0 -55
- package/src/wasm-jsr.ts +0 -280
package/src/enhanced-api.ts
DELETED
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Enhanced API patterns inspired by node-taglib
|
|
3
|
-
*
|
|
4
|
-
* This file demonstrates potential API improvements that could be added
|
|
5
|
-
* to taglib-wasm to improve developer experience.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { AudioFile, TagLibConfig } from "./types";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Enhanced error handling with error codes
|
|
12
|
-
*/
|
|
13
|
-
export interface TagLibError extends Error {
|
|
14
|
-
code:
|
|
15
|
-
| "FILE_NOT_FOUND"
|
|
16
|
-
| "INVALID_FORMAT"
|
|
17
|
-
| "MEMORY_ERROR"
|
|
18
|
-
| "PERMISSION_DENIED"
|
|
19
|
-
| "WASM_ERROR";
|
|
20
|
-
path?: string;
|
|
21
|
-
format?: string;
|
|
22
|
-
details?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Callback-style async operations (Node.js pattern)
|
|
27
|
-
*/
|
|
28
|
-
export type TagLibCallback<T> = (err: TagLibError | null, result?: T) => void;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Enhanced input types
|
|
32
|
-
*/
|
|
33
|
-
export type AudioInput =
|
|
34
|
-
| string // File path (Node.js/Deno/Bun only)
|
|
35
|
-
| Uint8Array // Raw bytes
|
|
36
|
-
| ArrayBuffer // ArrayBuffer
|
|
37
|
-
| Buffer // Node.js Buffer
|
|
38
|
-
| File; // Browser File object
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Enhanced TagLib class with multiple API patterns
|
|
42
|
-
*/
|
|
43
|
-
export class EnhancedTagLib {
|
|
44
|
-
/**
|
|
45
|
-
* Synchronous initialization (for server environments)
|
|
46
|
-
*/
|
|
47
|
-
static initializeSync(config?: TagLibConfig): EnhancedTagLib {
|
|
48
|
-
// Implementation would be synchronous version
|
|
49
|
-
throw new Error("Not implemented");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Asynchronous initialization (current pattern)
|
|
54
|
-
*/
|
|
55
|
-
static async initialize(config?: TagLibConfig): Promise<EnhancedTagLib> {
|
|
56
|
-
// Current implementation
|
|
57
|
-
throw new Error("Not implemented");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Open file with flexible input types
|
|
62
|
-
*/
|
|
63
|
-
openFile(input: AudioInput): EnhancedAudioFile {
|
|
64
|
-
if (typeof input === "string") {
|
|
65
|
-
return this.openFileFromPath(input);
|
|
66
|
-
} else if (input instanceof File) {
|
|
67
|
-
return this.openFileFromBrowserFile(input);
|
|
68
|
-
} else if (input instanceof ArrayBuffer) {
|
|
69
|
-
return this.openFileFromBuffer(new Uint8Array(input));
|
|
70
|
-
} else if (Buffer && Buffer.isBuffer(input)) {
|
|
71
|
-
return this.openFileFromBuffer(new Uint8Array(input));
|
|
72
|
-
} else {
|
|
73
|
-
return this.openFileFromBuffer(input);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Async file opening with callback pattern
|
|
79
|
-
*/
|
|
80
|
-
openFileAsync(
|
|
81
|
-
input: AudioInput,
|
|
82
|
-
callback: TagLibCallback<EnhancedAudioFile>,
|
|
83
|
-
): void {
|
|
84
|
-
try {
|
|
85
|
-
const file = this.openFile(input);
|
|
86
|
-
callback(null, file);
|
|
87
|
-
} catch (error) {
|
|
88
|
-
const tagLibError: TagLibError = {
|
|
89
|
-
name: "TagLibError",
|
|
90
|
-
message: (error as Error).message,
|
|
91
|
-
code: "INVALID_FORMAT",
|
|
92
|
-
details: (error as Error).stack,
|
|
93
|
-
};
|
|
94
|
-
callback(tagLibError);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Sync file opening (Node.js/Bun/Deno only)
|
|
100
|
-
*/
|
|
101
|
-
openFileSync(path: string): EnhancedAudioFile {
|
|
102
|
-
// Runtime-specific implementation
|
|
103
|
-
if (typeof (globalThis as any).Deno !== "undefined") {
|
|
104
|
-
const data = (globalThis as any).Deno.readFileSync(path);
|
|
105
|
-
return this.openFileFromBuffer(data);
|
|
106
|
-
} else if (typeof (globalThis as any).Bun !== "undefined") {
|
|
107
|
-
const file = (globalThis as any).Bun.file(path);
|
|
108
|
-
const data = new Uint8Array(file.arrayBufferSync());
|
|
109
|
-
return this.openFileFromBuffer(data);
|
|
110
|
-
} else if (typeof require !== "undefined") {
|
|
111
|
-
const fs = require("fs");
|
|
112
|
-
const data = fs.readFileSync(path);
|
|
113
|
-
return this.openFileFromBuffer(new Uint8Array(data));
|
|
114
|
-
} else {
|
|
115
|
-
throw new Error(
|
|
116
|
-
"Synchronous file reading not supported in browser environment",
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private openFileFromPath(path: string): EnhancedAudioFile {
|
|
122
|
-
throw new Error("Not implemented");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private openFileFromBrowserFile(file: File): EnhancedAudioFile {
|
|
126
|
-
throw new Error("Not implemented - requires async operation");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
private openFileFromBuffer(buffer: Uint8Array): EnhancedAudioFile {
|
|
130
|
-
throw new Error("Not implemented");
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Enhanced AudioFile with property accessors and better error handling
|
|
136
|
-
*/
|
|
137
|
-
export class EnhancedAudioFile {
|
|
138
|
-
// Property accessors (more intuitive than method calls)
|
|
139
|
-
get title(): string | undefined {
|
|
140
|
-
return this.tag().title;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
set title(value: string | undefined) {
|
|
144
|
-
if (value !== undefined) {
|
|
145
|
-
this.setTitle(value);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
get artist(): string | undefined {
|
|
150
|
-
return this.tag().artist;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
set artist(value: string | undefined) {
|
|
154
|
-
if (value !== undefined) {
|
|
155
|
-
this.setArtist(value);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
get album(): string | undefined {
|
|
160
|
-
return this.tag().album;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
set album(value: string | undefined) {
|
|
164
|
-
if (value !== undefined) {
|
|
165
|
-
this.setAlbum(value);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Async operations with callbacks
|
|
170
|
-
tagAsync(callback: TagLibCallback<any>): void {
|
|
171
|
-
try {
|
|
172
|
-
const tags = this.tag();
|
|
173
|
-
callback(null, tags);
|
|
174
|
-
} catch (error) {
|
|
175
|
-
const tagLibError: TagLibError = {
|
|
176
|
-
name: "TagLibError",
|
|
177
|
-
message: (error as Error).message,
|
|
178
|
-
code: "MEMORY_ERROR",
|
|
179
|
-
};
|
|
180
|
-
callback(tagLibError);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
saveAsync(callback: TagLibCallback<boolean>): void {
|
|
185
|
-
try {
|
|
186
|
-
const result = this.save();
|
|
187
|
-
callback(null, result);
|
|
188
|
-
} catch (error) {
|
|
189
|
-
const tagLibError: TagLibError = {
|
|
190
|
-
name: "TagLibError",
|
|
191
|
-
message: (error as Error).message,
|
|
192
|
-
code: "PERMISSION_DENIED",
|
|
193
|
-
};
|
|
194
|
-
callback(tagLibError);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Bulk tag setting (convenience method)
|
|
199
|
-
setTags(tags: {
|
|
200
|
-
title?: string;
|
|
201
|
-
artist?: string;
|
|
202
|
-
album?: string;
|
|
203
|
-
year?: number;
|
|
204
|
-
genre?: string;
|
|
205
|
-
track?: number;
|
|
206
|
-
comment?: string;
|
|
207
|
-
}): void {
|
|
208
|
-
Object.entries(tags).forEach(([key, value]) => {
|
|
209
|
-
if (value !== undefined) {
|
|
210
|
-
(this as any)[key] = value;
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Enhanced error handling for save operations
|
|
216
|
-
saveWithValidation(): { success: boolean; errors: TagLibError[] } {
|
|
217
|
-
const errors: TagLibError[] = [];
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
// Validate before saving
|
|
221
|
-
if (!this.isValid()) {
|
|
222
|
-
errors.push({
|
|
223
|
-
name: "TagLibError",
|
|
224
|
-
message: "File is not valid for writing",
|
|
225
|
-
code: "INVALID_FORMAT",
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const success = this.save();
|
|
230
|
-
return { success, errors };
|
|
231
|
-
} catch (error) {
|
|
232
|
-
errors.push({
|
|
233
|
-
name: "TagLibError",
|
|
234
|
-
message: (error as Error).message,
|
|
235
|
-
code: "PERMISSION_DENIED",
|
|
236
|
-
});
|
|
237
|
-
return { success: false, errors };
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Method stubs (would delegate to existing implementation)
|
|
242
|
-
private tag(): any {
|
|
243
|
-
throw new Error("Not implemented");
|
|
244
|
-
}
|
|
245
|
-
private setTitle(title: string): void {
|
|
246
|
-
throw new Error("Not implemented");
|
|
247
|
-
}
|
|
248
|
-
private setArtist(artist: string): void {
|
|
249
|
-
throw new Error("Not implemented");
|
|
250
|
-
}
|
|
251
|
-
private setAlbum(album: string): void {
|
|
252
|
-
throw new Error("Not implemented");
|
|
253
|
-
}
|
|
254
|
-
private save(): boolean {
|
|
255
|
-
throw new Error("Not implemented");
|
|
256
|
-
}
|
|
257
|
-
private isValid(): boolean {
|
|
258
|
-
throw new Error("Not implemented");
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Usage examples demonstrating enhanced API
|
|
264
|
-
*/
|
|
265
|
-
export function demonstrateEnhancedAPI() {
|
|
266
|
-
// Example 1: Property-based access
|
|
267
|
-
const file = new EnhancedAudioFile();
|
|
268
|
-
file.title = "New Song";
|
|
269
|
-
file.artist = "New Artist";
|
|
270
|
-
console.log(`${file.artist} - ${file.title}`);
|
|
271
|
-
|
|
272
|
-
// Example 2: Bulk tag setting
|
|
273
|
-
file.setTags({
|
|
274
|
-
title: "Song Title",
|
|
275
|
-
artist: "Artist Name",
|
|
276
|
-
album: "Album Name",
|
|
277
|
-
year: 2024,
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
// Example 3: Enhanced error handling
|
|
281
|
-
const result = file.saveWithValidation();
|
|
282
|
-
if (!result.success) {
|
|
283
|
-
result.errors.forEach((error) => {
|
|
284
|
-
console.error(`Error ${error.code}: ${error.message}`);
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Example 4: Callback-style async operations
|
|
289
|
-
file.tagAsync((err, tags) => {
|
|
290
|
-
if (err) {
|
|
291
|
-
console.error(`Failed to read tags: ${err.message}`);
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
console.log("Tags:", tags);
|
|
295
|
-
});
|
|
296
|
-
}
|
package/src/simple-jsr.ts
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview JSR-compatible simple API (uses taglib-jsr.ts instead of taglib.ts)
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { TagLib } from "./taglib-jsr.ts";
|
|
6
|
-
import type { AudioProperties, Tag } from "./types.ts";
|
|
7
|
-
|
|
8
|
-
// Cached TagLib instance for auto-initialization
|
|
9
|
-
let cachedTagLib: TagLib | null = null;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Get or create a TagLib instance with auto-initialization
|
|
13
|
-
*/
|
|
14
|
-
async function getTagLib(): Promise<TagLib> {
|
|
15
|
-
if (!cachedTagLib) {
|
|
16
|
-
cachedTagLib = await TagLib.initialize({
|
|
17
|
-
debug: false,
|
|
18
|
-
memory: {
|
|
19
|
-
initial: 16 * 1024 * 1024, // 16MB default
|
|
20
|
-
maximum: 64 * 1024 * 1024, // 64MB max
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
return cachedTagLib;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Read a file's data from various sources
|
|
29
|
-
*/
|
|
30
|
-
async function readFileData(file: string | Uint8Array | ArrayBuffer | File): Promise<Uint8Array> {
|
|
31
|
-
// Already a Uint8Array
|
|
32
|
-
if (file instanceof Uint8Array) {
|
|
33
|
-
return file;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ArrayBuffer - convert to Uint8Array
|
|
37
|
-
if (file instanceof ArrayBuffer) {
|
|
38
|
-
return new Uint8Array(file);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// File object (browser)
|
|
42
|
-
if (typeof File !== 'undefined' && file instanceof File) {
|
|
43
|
-
return new Uint8Array(await file.arrayBuffer());
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// String path - read from filesystem
|
|
47
|
-
if (typeof file === 'string') {
|
|
48
|
-
// Deno
|
|
49
|
-
if (typeof Deno !== 'undefined') {
|
|
50
|
-
return await Deno.readFile(file);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
throw new Error('File path reading not supported in this environment');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
throw new Error('Invalid file input type');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Read metadata tags from an audio file
|
|
61
|
-
*/
|
|
62
|
-
export async function readTags(file: string | Uint8Array | ArrayBuffer | File): Promise<Tag> {
|
|
63
|
-
const taglib = await getTagLib();
|
|
64
|
-
const audioData = await readFileData(file);
|
|
65
|
-
|
|
66
|
-
const audioFile = taglib.openFile(audioData);
|
|
67
|
-
try {
|
|
68
|
-
if (!audioFile.isValid()) {
|
|
69
|
-
throw new Error('Invalid audio file');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return audioFile.tag();
|
|
73
|
-
} finally {
|
|
74
|
-
audioFile.dispose();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Write metadata tags to an audio file
|
|
80
|
-
*/
|
|
81
|
-
export async function writeTags(
|
|
82
|
-
file: string | Uint8Array | ArrayBuffer | File,
|
|
83
|
-
tags: Partial<Tag>,
|
|
84
|
-
options?: number
|
|
85
|
-
): Promise<Uint8Array> {
|
|
86
|
-
const taglib = await getTagLib();
|
|
87
|
-
const audioData = await readFileData(file);
|
|
88
|
-
|
|
89
|
-
const audioFile = taglib.openFile(audioData);
|
|
90
|
-
try {
|
|
91
|
-
if (!audioFile.isValid()) {
|
|
92
|
-
throw new Error('Invalid audio file');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Write each tag if defined
|
|
96
|
-
if (tags.title !== undefined) audioFile.setTitle(tags.title);
|
|
97
|
-
if (tags.artist !== undefined) audioFile.setArtist(tags.artist);
|
|
98
|
-
if (tags.album !== undefined) audioFile.setAlbum(tags.album);
|
|
99
|
-
if (tags.comment !== undefined) audioFile.setComment(tags.comment);
|
|
100
|
-
if (tags.genre !== undefined) audioFile.setGenre(tags.genre);
|
|
101
|
-
if (tags.year !== undefined) audioFile.setYear(tags.year);
|
|
102
|
-
if (tags.track !== undefined) audioFile.setTrack(tags.track);
|
|
103
|
-
|
|
104
|
-
// Save changes to in-memory buffer
|
|
105
|
-
if (!audioFile.save()) {
|
|
106
|
-
throw new Error('Failed to save changes');
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return audioData;
|
|
110
|
-
} finally {
|
|
111
|
-
audioFile.dispose();
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Read audio properties from a file
|
|
117
|
-
*/
|
|
118
|
-
export async function readProperties(file: string | Uint8Array | ArrayBuffer | File): Promise<AudioProperties> {
|
|
119
|
-
const taglib = await getTagLib();
|
|
120
|
-
const audioData = await readFileData(file);
|
|
121
|
-
|
|
122
|
-
const audioFile = taglib.openFile(audioData);
|
|
123
|
-
try {
|
|
124
|
-
if (!audioFile.isValid()) {
|
|
125
|
-
throw new Error('Invalid audio file');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return audioFile.audioProperties();
|
|
129
|
-
} finally {
|
|
130
|
-
audioFile.dispose();
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Tag field constants for go-taglib compatibility
|
|
136
|
-
*/
|
|
137
|
-
export const Title = "title";
|
|
138
|
-
export const Artist = "artist";
|
|
139
|
-
export const Album = "album";
|
|
140
|
-
export const Comment = "comment";
|
|
141
|
-
export const Genre = "genre";
|
|
142
|
-
export const Year = "year";
|
|
143
|
-
export const Track = "track";
|
|
144
|
-
export const AlbumArtist = "albumartist";
|
|
145
|
-
export const Composer = "composer";
|
|
146
|
-
export const DiscNumber = "discnumber";
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Check if a file is a valid audio file
|
|
150
|
-
*/
|
|
151
|
-
export async function isValidAudioFile(file: string | Uint8Array | ArrayBuffer | File): Promise<boolean> {
|
|
152
|
-
try {
|
|
153
|
-
const taglib = await getTagLib();
|
|
154
|
-
const audioData = await readFileData(file);
|
|
155
|
-
|
|
156
|
-
const audioFile = taglib.openFile(audioData);
|
|
157
|
-
const valid = audioFile.isValid();
|
|
158
|
-
audioFile.dispose();
|
|
159
|
-
|
|
160
|
-
return valid;
|
|
161
|
-
} catch {
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Get the audio format of a file
|
|
168
|
-
*/
|
|
169
|
-
export async function getFormat(file: string | Uint8Array | ArrayBuffer | File): Promise<string | undefined> {
|
|
170
|
-
const taglib = await getTagLib();
|
|
171
|
-
const audioData = await readFileData(file);
|
|
172
|
-
|
|
173
|
-
const audioFile = taglib.openFile(audioData);
|
|
174
|
-
try {
|
|
175
|
-
if (!audioFile.isValid()) {
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return audioFile.format();
|
|
180
|
-
} finally {
|
|
181
|
-
audioFile.dispose();
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Clear all tags from a file
|
|
187
|
-
*/
|
|
188
|
-
export async function clearTags(file: string | Uint8Array | ArrayBuffer | File): Promise<Uint8Array> {
|
|
189
|
-
return writeTags(file, {
|
|
190
|
-
title: "",
|
|
191
|
-
artist: "",
|
|
192
|
-
album: "",
|
|
193
|
-
comment: "",
|
|
194
|
-
genre: "",
|
|
195
|
-
year: 0,
|
|
196
|
-
track: 0,
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Type exports for convenience
|
|
201
|
-
export type { Tag, AudioProperties } from "./types.ts";
|
package/src/taglib-embind.ts
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
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
|
-
}
|