taglib-wasm 0.1.0
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/LICENSE +21 -0
- package/README.md +452 -0
- package/build/taglib.js +35 -0
- package/build/taglib.wasm +0 -0
- package/lib/taglib/COPYING.LGPL +502 -0
- package/lib/taglib/COPYING.MPL +470 -0
- package/lib/taglib/README.md +24 -0
- package/package.json +63 -0
- package/src/enhanced-api.ts +274 -0
- package/src/mod.ts +8 -0
- package/src/taglib.ts +441 -0
- package/src/types.ts +353 -0
- package/src/wasm.ts +232 -0
|
@@ -0,0 +1,274 @@
|
|
|
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.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Enhanced error handling with error codes
|
|
12
|
+
*/
|
|
13
|
+
export interface TagLibError extends Error {
|
|
14
|
+
code: 'FILE_NOT_FOUND' | 'INVALID_FORMAT' | 'MEMORY_ERROR' | 'PERMISSION_DENIED' | 'WASM_ERROR';
|
|
15
|
+
path?: string;
|
|
16
|
+
format?: string;
|
|
17
|
+
details?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Callback-style async operations (Node.js pattern)
|
|
22
|
+
*/
|
|
23
|
+
export type TagLibCallback<T> = (err: TagLibError | null, result?: T) => void;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Enhanced input types
|
|
27
|
+
*/
|
|
28
|
+
export type AudioInput =
|
|
29
|
+
| string // File path (Node.js/Deno/Bun only)
|
|
30
|
+
| Uint8Array // Raw bytes
|
|
31
|
+
| ArrayBuffer // ArrayBuffer
|
|
32
|
+
| Buffer // Node.js Buffer
|
|
33
|
+
| File; // Browser File object
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Enhanced TagLib class with multiple API patterns
|
|
37
|
+
*/
|
|
38
|
+
export class EnhancedTagLib {
|
|
39
|
+
/**
|
|
40
|
+
* Synchronous initialization (for server environments)
|
|
41
|
+
*/
|
|
42
|
+
static initializeSync(config?: TagLibConfig): EnhancedTagLib {
|
|
43
|
+
// Implementation would be synchronous version
|
|
44
|
+
throw new Error("Not implemented");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Asynchronous initialization (current pattern)
|
|
49
|
+
*/
|
|
50
|
+
static async initialize(config?: TagLibConfig): Promise<EnhancedTagLib> {
|
|
51
|
+
// Current implementation
|
|
52
|
+
throw new Error("Not implemented");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Open file with flexible input types
|
|
57
|
+
*/
|
|
58
|
+
openFile(input: AudioInput): EnhancedAudioFile {
|
|
59
|
+
if (typeof input === 'string') {
|
|
60
|
+
return this.openFileFromPath(input);
|
|
61
|
+
} else if (input instanceof File) {
|
|
62
|
+
return this.openFileFromBrowserFile(input);
|
|
63
|
+
} else if (input instanceof ArrayBuffer) {
|
|
64
|
+
return this.openFileFromBuffer(new Uint8Array(input));
|
|
65
|
+
} else if (Buffer && Buffer.isBuffer(input)) {
|
|
66
|
+
return this.openFileFromBuffer(new Uint8Array(input));
|
|
67
|
+
} else {
|
|
68
|
+
return this.openFileFromBuffer(input);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Async file opening with callback pattern
|
|
74
|
+
*/
|
|
75
|
+
openFileAsync(input: AudioInput, callback: TagLibCallback<EnhancedAudioFile>): void {
|
|
76
|
+
try {
|
|
77
|
+
const file = this.openFile(input);
|
|
78
|
+
callback(null, file);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const tagLibError: TagLibError = {
|
|
81
|
+
name: 'TagLibError',
|
|
82
|
+
message: error.message,
|
|
83
|
+
code: 'INVALID_FORMAT',
|
|
84
|
+
details: error.stack,
|
|
85
|
+
};
|
|
86
|
+
callback(tagLibError);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Sync file opening (Node.js/Bun/Deno only)
|
|
92
|
+
*/
|
|
93
|
+
openFileSync(path: string): EnhancedAudioFile {
|
|
94
|
+
// Runtime-specific implementation
|
|
95
|
+
if (typeof Deno !== 'undefined') {
|
|
96
|
+
const data = Deno.readFileSync(path);
|
|
97
|
+
return this.openFileFromBuffer(data);
|
|
98
|
+
} else if (typeof Bun !== 'undefined') {
|
|
99
|
+
const file = Bun.file(path);
|
|
100
|
+
const data = new Uint8Array(file.arrayBufferSync());
|
|
101
|
+
return this.openFileFromBuffer(data);
|
|
102
|
+
} else if (typeof require !== 'undefined') {
|
|
103
|
+
const fs = require('fs');
|
|
104
|
+
const data = fs.readFileSync(path);
|
|
105
|
+
return this.openFileFromBuffer(new Uint8Array(data));
|
|
106
|
+
} else {
|
|
107
|
+
throw new Error('Synchronous file reading not supported in browser environment');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private openFileFromPath(path: string): EnhancedAudioFile {
|
|
112
|
+
throw new Error("Not implemented");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private openFileFromBrowserFile(file: File): EnhancedAudioFile {
|
|
116
|
+
throw new Error("Not implemented - requires async operation");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private openFileFromBuffer(buffer: Uint8Array): EnhancedAudioFile {
|
|
120
|
+
throw new Error("Not implemented");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Enhanced AudioFile with property accessors and better error handling
|
|
126
|
+
*/
|
|
127
|
+
export class EnhancedAudioFile {
|
|
128
|
+
// Property accessors (more intuitive than method calls)
|
|
129
|
+
get title(): string | undefined {
|
|
130
|
+
return this.tag().title;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
set title(value: string | undefined) {
|
|
134
|
+
if (value !== undefined) {
|
|
135
|
+
this.setTitle(value);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
get artist(): string | undefined {
|
|
140
|
+
return this.tag().artist;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
set artist(value: string | undefined) {
|
|
144
|
+
if (value !== undefined) {
|
|
145
|
+
this.setArtist(value);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
get album(): string | undefined {
|
|
150
|
+
return this.tag().album;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
set album(value: string | undefined) {
|
|
154
|
+
if (value !== undefined) {
|
|
155
|
+
this.setAlbum(value);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Async operations with callbacks
|
|
160
|
+
tagAsync(callback: TagLibCallback<any>): void {
|
|
161
|
+
try {
|
|
162
|
+
const tags = this.tag();
|
|
163
|
+
callback(null, tags);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
const tagLibError: TagLibError = {
|
|
166
|
+
name: 'TagLibError',
|
|
167
|
+
message: error.message,
|
|
168
|
+
code: 'MEMORY_ERROR',
|
|
169
|
+
};
|
|
170
|
+
callback(tagLibError);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
saveAsync(callback: TagLibCallback<boolean>): void {
|
|
175
|
+
try {
|
|
176
|
+
const result = this.save();
|
|
177
|
+
callback(null, result);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
const tagLibError: TagLibError = {
|
|
180
|
+
name: 'TagLibError',
|
|
181
|
+
message: error.message,
|
|
182
|
+
code: 'PERMISSION_DENIED',
|
|
183
|
+
};
|
|
184
|
+
callback(tagLibError);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Bulk tag setting (convenience method)
|
|
189
|
+
setTags(tags: {
|
|
190
|
+
title?: string;
|
|
191
|
+
artist?: string;
|
|
192
|
+
album?: string;
|
|
193
|
+
year?: number;
|
|
194
|
+
genre?: string;
|
|
195
|
+
track?: number;
|
|
196
|
+
comment?: string;
|
|
197
|
+
}): void {
|
|
198
|
+
Object.entries(tags).forEach(([key, value]) => {
|
|
199
|
+
if (value !== undefined) {
|
|
200
|
+
(this as any)[key] = value;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Enhanced error handling for save operations
|
|
206
|
+
saveWithValidation(): { success: boolean; errors: TagLibError[] } {
|
|
207
|
+
const errors: TagLibError[] = [];
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
// Validate before saving
|
|
211
|
+
if (!this.isValid()) {
|
|
212
|
+
errors.push({
|
|
213
|
+
name: 'TagLibError',
|
|
214
|
+
message: 'File is not valid for writing',
|
|
215
|
+
code: 'INVALID_FORMAT',
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const success = this.save();
|
|
220
|
+
return { success, errors };
|
|
221
|
+
} catch (error) {
|
|
222
|
+
errors.push({
|
|
223
|
+
name: 'TagLibError',
|
|
224
|
+
message: error.message,
|
|
225
|
+
code: 'PERMISSION_DENIED',
|
|
226
|
+
});
|
|
227
|
+
return { success: false, errors };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Method stubs (would delegate to existing implementation)
|
|
232
|
+
private tag(): any { throw new Error("Not implemented"); }
|
|
233
|
+
private setTitle(title: string): void { throw new Error("Not implemented"); }
|
|
234
|
+
private setArtist(artist: string): void { throw new Error("Not implemented"); }
|
|
235
|
+
private setAlbum(album: string): void { throw new Error("Not implemented"); }
|
|
236
|
+
private save(): boolean { throw new Error("Not implemented"); }
|
|
237
|
+
private isValid(): boolean { throw new Error("Not implemented"); }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Usage examples demonstrating enhanced API
|
|
242
|
+
*/
|
|
243
|
+
export function demonstrateEnhancedAPI() {
|
|
244
|
+
// Example 1: Property-based access
|
|
245
|
+
const file = new EnhancedAudioFile();
|
|
246
|
+
file.title = "New Song";
|
|
247
|
+
file.artist = "New Artist";
|
|
248
|
+
console.log(`${file.artist} - ${file.title}`);
|
|
249
|
+
|
|
250
|
+
// Example 2: Bulk tag setting
|
|
251
|
+
file.setTags({
|
|
252
|
+
title: "Song Title",
|
|
253
|
+
artist: "Artist Name",
|
|
254
|
+
album: "Album Name",
|
|
255
|
+
year: 2024,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Example 3: Enhanced error handling
|
|
259
|
+
const result = file.saveWithValidation();
|
|
260
|
+
if (!result.success) {
|
|
261
|
+
result.errors.forEach(error => {
|
|
262
|
+
console.error(`Error ${error.code}: ${error.message}`);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Example 4: Callback-style async operations
|
|
267
|
+
file.tagAsync((err, tags) => {
|
|
268
|
+
if (err) {
|
|
269
|
+
console.error(`Failed to read tags: ${err.message}`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
console.log('Tags:', tags);
|
|
273
|
+
});
|
|
274
|
+
}
|
package/src/mod.ts
ADDED
package/src/taglib.ts
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main TagLib API wrapper
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
AudioFormat,
|
|
7
|
+
AudioProperties,
|
|
8
|
+
ExtendedTag,
|
|
9
|
+
FieldMapping,
|
|
10
|
+
Picture,
|
|
11
|
+
PropertyMap,
|
|
12
|
+
Tag,
|
|
13
|
+
TagLibConfig,
|
|
14
|
+
} from "./types.ts";
|
|
15
|
+
import { METADATA_MAPPINGS } from "./types.ts";
|
|
16
|
+
import {
|
|
17
|
+
cStringToJS,
|
|
18
|
+
jsToCString,
|
|
19
|
+
loadTagLibModule,
|
|
20
|
+
type TagLibModule,
|
|
21
|
+
} from "./wasm.ts";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Represents an audio file with metadata and properties
|
|
25
|
+
*/
|
|
26
|
+
export class AudioFile {
|
|
27
|
+
private module: TagLibModule;
|
|
28
|
+
private fileId: number;
|
|
29
|
+
private tagPtr: number;
|
|
30
|
+
private propsPtr: number;
|
|
31
|
+
|
|
32
|
+
constructor(module: TagLibModule, fileId: number) {
|
|
33
|
+
this.module = module;
|
|
34
|
+
this.fileId = fileId;
|
|
35
|
+
this.tagPtr = module._taglib_file_tag(fileId);
|
|
36
|
+
this.propsPtr = module._taglib_file_audioproperties(fileId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if the file is valid and was loaded successfully
|
|
41
|
+
*/
|
|
42
|
+
isValid(): boolean {
|
|
43
|
+
return this.module._taglib_file_is_valid(this.fileId) !== 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get the file format
|
|
48
|
+
*/
|
|
49
|
+
format(): AudioFormat {
|
|
50
|
+
const formatPtr = this.module._taglib_file_format(this.fileId);
|
|
51
|
+
if (formatPtr === 0) return "MP3"; // fallback
|
|
52
|
+
const formatStr = cStringToJS(this.module, formatPtr);
|
|
53
|
+
return formatStr as AudioFormat;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get basic tag information
|
|
58
|
+
*/
|
|
59
|
+
tag(): Tag {
|
|
60
|
+
if (this.tagPtr === 0) return {};
|
|
61
|
+
|
|
62
|
+
const title = this.module._taglib_tag_title(this.tagPtr);
|
|
63
|
+
const artist = this.module._taglib_tag_artist(this.tagPtr);
|
|
64
|
+
const album = this.module._taglib_tag_album(this.tagPtr);
|
|
65
|
+
const comment = this.module._taglib_tag_comment(this.tagPtr);
|
|
66
|
+
const genre = this.module._taglib_tag_genre(this.tagPtr);
|
|
67
|
+
const year = this.module._taglib_tag_year(this.tagPtr);
|
|
68
|
+
const track = this.module._taglib_tag_track(this.tagPtr);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
title: title ? cStringToJS(this.module, title) : undefined,
|
|
72
|
+
artist: artist ? cStringToJS(this.module, artist) : undefined,
|
|
73
|
+
album: album ? cStringToJS(this.module, album) : undefined,
|
|
74
|
+
comment: comment ? cStringToJS(this.module, comment) : undefined,
|
|
75
|
+
genre: genre ? cStringToJS(this.module, genre) : undefined,
|
|
76
|
+
year: year || undefined,
|
|
77
|
+
track: track || undefined,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get audio properties (duration, bitrate, etc.)
|
|
83
|
+
*/
|
|
84
|
+
audioProperties(): AudioProperties | null {
|
|
85
|
+
if (this.propsPtr === 0) return null;
|
|
86
|
+
|
|
87
|
+
const length = this.module._taglib_audioproperties_length(this.propsPtr);
|
|
88
|
+
const bitrate = this.module._taglib_audioproperties_bitrate(this.propsPtr);
|
|
89
|
+
const sampleRate = this.module._taglib_audioproperties_samplerate(
|
|
90
|
+
this.propsPtr,
|
|
91
|
+
);
|
|
92
|
+
const channels = this.module._taglib_audioproperties_channels(
|
|
93
|
+
this.propsPtr,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
length,
|
|
98
|
+
bitrate,
|
|
99
|
+
sampleRate,
|
|
100
|
+
channels,
|
|
101
|
+
format: this.format(),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Set the title tag
|
|
107
|
+
*/
|
|
108
|
+
setTitle(title: string): void {
|
|
109
|
+
if (this.tagPtr === 0) return;
|
|
110
|
+
const titlePtr = jsToCString(this.module, title);
|
|
111
|
+
this.module._taglib_tag_set_title(this.tagPtr, titlePtr);
|
|
112
|
+
this.module._free(titlePtr);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Set the artist tag
|
|
117
|
+
*/
|
|
118
|
+
setArtist(artist: string): void {
|
|
119
|
+
if (this.tagPtr === 0) return;
|
|
120
|
+
const artistPtr = jsToCString(this.module, artist);
|
|
121
|
+
this.module._taglib_tag_set_artist(this.tagPtr, artistPtr);
|
|
122
|
+
this.module._free(artistPtr);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Set the album tag
|
|
127
|
+
*/
|
|
128
|
+
setAlbum(album: string): void {
|
|
129
|
+
if (this.tagPtr === 0) return;
|
|
130
|
+
const albumPtr = jsToCString(this.module, album);
|
|
131
|
+
this.module._taglib_tag_set_album(this.tagPtr, albumPtr);
|
|
132
|
+
this.module._free(albumPtr);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Set the comment tag
|
|
137
|
+
*/
|
|
138
|
+
setComment(comment: string): void {
|
|
139
|
+
if (this.tagPtr === 0) return;
|
|
140
|
+
const commentPtr = jsToCString(this.module, comment);
|
|
141
|
+
this.module._taglib_tag_set_comment(this.tagPtr, commentPtr);
|
|
142
|
+
this.module._free(commentPtr);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Set the genre tag
|
|
147
|
+
*/
|
|
148
|
+
setGenre(genre: string): void {
|
|
149
|
+
if (this.tagPtr === 0) return;
|
|
150
|
+
const genrePtr = jsToCString(this.module, genre);
|
|
151
|
+
this.module._taglib_tag_set_genre(this.tagPtr, genrePtr);
|
|
152
|
+
this.module._free(genrePtr);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Set the year tag
|
|
157
|
+
*/
|
|
158
|
+
setYear(year: number): void {
|
|
159
|
+
if (this.tagPtr === 0) return;
|
|
160
|
+
this.module._taglib_tag_set_year(this.tagPtr, year);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Set the track number tag
|
|
165
|
+
*/
|
|
166
|
+
setTrack(track: number): void {
|
|
167
|
+
if (this.tagPtr === 0) return;
|
|
168
|
+
this.module._taglib_tag_set_track(this.tagPtr, track);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Save changes to the file
|
|
173
|
+
*/
|
|
174
|
+
save(): boolean {
|
|
175
|
+
return this.module._taglib_file_save(this.fileId) !== 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get extended metadata with format-agnostic field names
|
|
180
|
+
*/
|
|
181
|
+
extendedTag(): ExtendedTag {
|
|
182
|
+
// Get basic tags first
|
|
183
|
+
const basicTag = this.tag();
|
|
184
|
+
|
|
185
|
+
// TODO: Implement advanced metadata reading via PropertyMap
|
|
186
|
+
// For now, return basic tags with placeholder for advanced fields
|
|
187
|
+
return {
|
|
188
|
+
...basicTag,
|
|
189
|
+
// Advanced fields would be populated by PropertyMap reading
|
|
190
|
+
acoustidFingerprint: undefined,
|
|
191
|
+
acoustidId: undefined,
|
|
192
|
+
musicbrainzTrackId: undefined,
|
|
193
|
+
musicbrainzReleaseId: undefined,
|
|
194
|
+
musicbrainzArtistId: undefined,
|
|
195
|
+
musicbrainzReleaseGroupId: undefined,
|
|
196
|
+
albumArtist: undefined,
|
|
197
|
+
composer: undefined,
|
|
198
|
+
discNumber: undefined,
|
|
199
|
+
totalTracks: undefined,
|
|
200
|
+
totalDiscs: undefined,
|
|
201
|
+
bpm: undefined,
|
|
202
|
+
compilation: undefined,
|
|
203
|
+
titleSort: undefined,
|
|
204
|
+
artistSort: undefined,
|
|
205
|
+
albumSort: undefined,
|
|
206
|
+
// ReplayGain fields
|
|
207
|
+
replayGainTrackGain: undefined,
|
|
208
|
+
replayGainTrackPeak: undefined,
|
|
209
|
+
replayGainAlbumGain: undefined,
|
|
210
|
+
replayGainAlbumPeak: undefined,
|
|
211
|
+
// Apple Sound Check
|
|
212
|
+
appleSoundCheck: undefined,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Set extended metadata using format-agnostic field names
|
|
218
|
+
* The library automatically maps fields to the correct format-specific storage
|
|
219
|
+
*/
|
|
220
|
+
setExtendedTag(tag: Partial<ExtendedTag>): void {
|
|
221
|
+
// Set basic tags using existing methods
|
|
222
|
+
if (tag.title !== undefined) this.setTitle(tag.title);
|
|
223
|
+
if (tag.artist !== undefined) this.setArtist(tag.artist);
|
|
224
|
+
if (tag.album !== undefined) this.setAlbum(tag.album);
|
|
225
|
+
if (tag.comment !== undefined) this.setComment(tag.comment);
|
|
226
|
+
if (tag.genre !== undefined) this.setGenre(tag.genre);
|
|
227
|
+
if (tag.year !== undefined) this.setYear(tag.year);
|
|
228
|
+
if (tag.track !== undefined) this.setTrack(tag.track);
|
|
229
|
+
|
|
230
|
+
// TODO: Implement advanced metadata writing via PropertyMap
|
|
231
|
+
// For fields like acoustidFingerprint, acoustidId, etc.
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Set AcoustID fingerprint (format-agnostic)
|
|
236
|
+
* Automatically stores in the correct location for each format:
|
|
237
|
+
* - MP3: TXXX frame with "Acoustid Fingerprint" description
|
|
238
|
+
* - FLAC/OGG: ACOUSTID_FINGERPRINT Vorbis comment
|
|
239
|
+
* - MP4: ----:com.apple.iTunes:Acoustid Fingerprint atom
|
|
240
|
+
*/
|
|
241
|
+
setAcoustidFingerprint(fingerprint: string): void {
|
|
242
|
+
// TODO: Implement format-specific storage
|
|
243
|
+
console.warn("setAcoustidFingerprint: Advanced metadata not yet implemented");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get AcoustID fingerprint (format-agnostic)
|
|
248
|
+
*/
|
|
249
|
+
getAcoustidFingerprint(): string | undefined {
|
|
250
|
+
// TODO: Implement format-specific reading
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Set AcoustID UUID (format-agnostic)
|
|
256
|
+
* Automatically stores in the correct location for each format:
|
|
257
|
+
* - MP3: TXXX frame with "Acoustid Id" description
|
|
258
|
+
* - FLAC/OGG: ACOUSTID_ID Vorbis comment
|
|
259
|
+
* - MP4: ----:com.apple.iTunes:Acoustid Id atom
|
|
260
|
+
*/
|
|
261
|
+
setAcoustidId(id: string): void {
|
|
262
|
+
// TODO: Implement format-specific storage
|
|
263
|
+
console.warn("setAcoustidId: Advanced metadata not yet implemented");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get AcoustID UUID (format-agnostic)
|
|
268
|
+
*/
|
|
269
|
+
getAcoustidId(): string | undefined {
|
|
270
|
+
// TODO: Implement format-specific reading
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Set MusicBrainz Track ID (format-agnostic)
|
|
276
|
+
*/
|
|
277
|
+
setMusicBrainzTrackId(id: string): void {
|
|
278
|
+
// TODO: Implement format-specific storage
|
|
279
|
+
console.warn("setMusicBrainzTrackId: Advanced metadata not yet implemented");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get MusicBrainz Track ID (format-agnostic)
|
|
284
|
+
*/
|
|
285
|
+
getMusicBrainzTrackId(): string | undefined {
|
|
286
|
+
// TODO: Implement format-specific reading
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Set ReplayGain track gain (format-agnostic)
|
|
292
|
+
* Automatically stores in the correct location for each format:
|
|
293
|
+
* - MP3: TXXX frame with "ReplayGain_Track_Gain" description
|
|
294
|
+
* - FLAC/OGG: REPLAYGAIN_TRACK_GAIN Vorbis comment
|
|
295
|
+
* - MP4: ----:com.apple.iTunes:replaygain_track_gain atom
|
|
296
|
+
*/
|
|
297
|
+
setReplayGainTrackGain(gain: string): void {
|
|
298
|
+
// TODO: Implement format-specific storage
|
|
299
|
+
console.warn("setReplayGainTrackGain: Advanced metadata not yet implemented");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get ReplayGain track gain (format-agnostic)
|
|
304
|
+
*/
|
|
305
|
+
getReplayGainTrackGain(): string | undefined {
|
|
306
|
+
// TODO: Implement format-specific reading
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Set ReplayGain track peak (format-agnostic)
|
|
312
|
+
*/
|
|
313
|
+
setReplayGainTrackPeak(peak: string): void {
|
|
314
|
+
// TODO: Implement format-specific storage
|
|
315
|
+
console.warn("setReplayGainTrackPeak: Advanced metadata not yet implemented");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Get ReplayGain track peak (format-agnostic)
|
|
320
|
+
*/
|
|
321
|
+
getReplayGainTrackPeak(): string | undefined {
|
|
322
|
+
// TODO: Implement format-specific reading
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Set ReplayGain album gain (format-agnostic)
|
|
328
|
+
*/
|
|
329
|
+
setReplayGainAlbumGain(gain: string): void {
|
|
330
|
+
// TODO: Implement format-specific storage
|
|
331
|
+
console.warn("setReplayGainAlbumGain: Advanced metadata not yet implemented");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get ReplayGain album gain (format-agnostic)
|
|
336
|
+
*/
|
|
337
|
+
getReplayGainAlbumGain(): string | undefined {
|
|
338
|
+
// TODO: Implement format-specific reading
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Set ReplayGain album peak (format-agnostic)
|
|
344
|
+
*/
|
|
345
|
+
setReplayGainAlbumPeak(peak: string): void {
|
|
346
|
+
// TODO: Implement format-specific storage
|
|
347
|
+
console.warn("setReplayGainAlbumPeak: Advanced metadata not yet implemented");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Get ReplayGain album peak (format-agnostic)
|
|
352
|
+
*/
|
|
353
|
+
getReplayGainAlbumPeak(): string | undefined {
|
|
354
|
+
// TODO: Implement format-specific reading
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Set Apple Sound Check normalization data (format-agnostic)
|
|
360
|
+
* Automatically stores in the correct location for each format:
|
|
361
|
+
* - MP3: TXXX frame with "iTunNORM" description
|
|
362
|
+
* - FLAC/OGG: ITUNNORM Vorbis comment
|
|
363
|
+
* - MP4: ----:com.apple.iTunes:iTunNORM atom
|
|
364
|
+
*/
|
|
365
|
+
setAppleSoundCheck(iTunNORM: string): void {
|
|
366
|
+
// TODO: Implement format-specific storage
|
|
367
|
+
console.warn("setAppleSoundCheck: Advanced metadata not yet implemented");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Get Apple Sound Check normalization data (format-agnostic)
|
|
372
|
+
*/
|
|
373
|
+
getAppleSoundCheck(): string | undefined {
|
|
374
|
+
// TODO: Implement format-specific reading
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Clean up resources
|
|
380
|
+
*/
|
|
381
|
+
dispose(): void {
|
|
382
|
+
if (this.fileId !== 0) {
|
|
383
|
+
this.module._taglib_file_delete(this.fileId);
|
|
384
|
+
this.fileId = 0;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Main TagLib class for creating and managing audio files
|
|
391
|
+
*/
|
|
392
|
+
export class TagLib {
|
|
393
|
+
private module: TagLibModule;
|
|
394
|
+
|
|
395
|
+
private constructor(module: TagLibModule) {
|
|
396
|
+
this.module = module;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Initialize TagLib with optional configuration
|
|
401
|
+
*/
|
|
402
|
+
static async initialize(config?: TagLibConfig): Promise<TagLib> {
|
|
403
|
+
const module = await loadTagLibModule(config);
|
|
404
|
+
return new TagLib(module);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Open an audio file from a buffer
|
|
409
|
+
*/
|
|
410
|
+
openFile(buffer: Uint8Array): AudioFile {
|
|
411
|
+
if (!this.module.HEAPU8) {
|
|
412
|
+
throw new Error("WASM module not properly initialized - missing HEAPU8");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Use Emscripten's allocate function for proper memory management
|
|
416
|
+
const dataPtr = this.module.allocate(buffer, this.module.ALLOC_NORMAL);
|
|
417
|
+
|
|
418
|
+
const fileId = this.module._taglib_file_new_from_buffer(
|
|
419
|
+
dataPtr,
|
|
420
|
+
buffer.length,
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
if (fileId === 0) {
|
|
424
|
+
// Don't free memory immediately to debug reuse issue
|
|
425
|
+
console.log(`DEBUG: File creation failed, not freeing memory at ${dataPtr}`);
|
|
426
|
+
throw new Error("Failed to open audio file - invalid format or corrupted data");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Free the temporary buffer copy (TagLib has made its own copy in ByteVector)
|
|
430
|
+
this.module._free(dataPtr);
|
|
431
|
+
|
|
432
|
+
return new AudioFile(this.module, fileId);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Get the underlying WASM module (for advanced usage)
|
|
437
|
+
*/
|
|
438
|
+
getModule(): TagLibModule {
|
|
439
|
+
return this.module;
|
|
440
|
+
}
|
|
441
|
+
}
|