taglib-wasm 0.2.7 → 0.2.8
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 +18 -52
- package/build/taglib.js +4 -10
- package/build/taglib.wasm +0 -0
- package/index.ts +13 -3
- package/package.json +3 -1
- package/src/simple-jsr.ts +40 -37
- package/src/simple.ts +19 -22
- package/src/taglib-jsr.ts +22 -518
- package/src/taglib.ts +160 -505
- package/src/types.ts +6 -3
- package/src/wasm-jsr.ts +12 -279
- package/src/wasm.ts +75 -244
- package/src/enhanced-api.ts +0 -296
- package/src/taglib-embind.ts +0 -231
- package/src/wasm-embind.ts +0 -55
package/src/taglib.ts
CHANGED
|
@@ -1,606 +1,261 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { TagLibModule, WasmModule } from "./wasm-jsr.ts";
|
|
2
|
+
import type { AudioProperties, FileType, PropertyMap, Tag as BasicTag } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
// Extended Tag interface with setters
|
|
5
|
+
export interface Tag extends BasicTag {
|
|
6
|
+
setTitle(value: string): void;
|
|
7
|
+
setArtist(value: string): void;
|
|
8
|
+
setAlbum(value: string): void;
|
|
9
|
+
setComment(value: string): void;
|
|
10
|
+
setGenre(value: string): void;
|
|
11
|
+
setYear(value: number): void;
|
|
12
|
+
setTrack(value: number): void;
|
|
13
|
+
}
|
|
4
14
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
PropertyMap
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
loadTagLibModule,
|
|
25
|
-
type TagLibModule,
|
|
26
|
-
} from "./wasm.ts";
|
|
15
|
+
// Re-export types for JSR
|
|
16
|
+
export type { AudioProperties, FileType, PropertyMap } from "./types.ts";
|
|
17
|
+
|
|
18
|
+
export interface AudioFile {
|
|
19
|
+
getFormat(): FileType;
|
|
20
|
+
tag(): Tag;
|
|
21
|
+
audioProperties(): AudioProperties | null;
|
|
22
|
+
properties(): PropertyMap;
|
|
23
|
+
setProperties(properties: PropertyMap): void;
|
|
24
|
+
getProperty(key: string): string | undefined;
|
|
25
|
+
setProperty(key: string, value: string): void;
|
|
26
|
+
isMP4(): boolean;
|
|
27
|
+
getMP4Item(key: string): string | undefined;
|
|
28
|
+
setMP4Item(key: string, value: string): void;
|
|
29
|
+
removeMP4Item(key: string): void;
|
|
30
|
+
save(): boolean;
|
|
31
|
+
isValid(): boolean;
|
|
32
|
+
dispose(): void;
|
|
33
|
+
}
|
|
27
34
|
|
|
28
35
|
/**
|
|
29
|
-
*
|
|
36
|
+
* Audio file wrapper using Embind API
|
|
30
37
|
*/
|
|
31
|
-
export class AudioFile {
|
|
32
|
-
private
|
|
33
|
-
private
|
|
34
|
-
private
|
|
35
|
-
private propsPtr: number;
|
|
38
|
+
export class AudioFileImpl implements AudioFile {
|
|
39
|
+
private fileHandle: any;
|
|
40
|
+
private cachedTag: Tag | null = null;
|
|
41
|
+
private cachedAudioProperties: AudioProperties | null = null;
|
|
36
42
|
|
|
37
|
-
constructor(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.
|
|
43
|
+
constructor(
|
|
44
|
+
private module: TagLibModule,
|
|
45
|
+
fileHandle: any,
|
|
46
|
+
) {
|
|
47
|
+
this.fileHandle = fileHandle;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
/**
|
|
45
|
-
*
|
|
51
|
+
* Get file format
|
|
46
52
|
*/
|
|
47
|
-
|
|
48
|
-
return this.
|
|
53
|
+
getFormat(): FileType {
|
|
54
|
+
return this.fileHandle.getFormat() as FileType;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
/**
|
|
52
|
-
* Get
|
|
53
|
-
*/
|
|
54
|
-
format(): AudioFormat {
|
|
55
|
-
const formatPtr = this.module._taglib_file_format(this.fileId);
|
|
56
|
-
if (formatPtr === 0) return "MP3"; // fallback
|
|
57
|
-
const formatStr = cStringToJS(this.module, formatPtr);
|
|
58
|
-
return formatStr as AudioFormat;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Get basic tag information
|
|
58
|
+
* Get tag object for reading/writing metadata
|
|
63
59
|
*/
|
|
64
60
|
tag(): Tag {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const comment = this.module._taglib_tag_comment(this.tagPtr);
|
|
71
|
-
const genre = this.module._taglib_tag_genre(this.tagPtr);
|
|
72
|
-
const year = this.module._taglib_tag_year(this.tagPtr);
|
|
73
|
-
const track = this.module._taglib_tag_track(this.tagPtr);
|
|
74
|
-
|
|
61
|
+
const tagWrapper = this.fileHandle.getTag();
|
|
62
|
+
if (!tagWrapper) {
|
|
63
|
+
throw new Error("Failed to get tag from file");
|
|
64
|
+
}
|
|
65
|
+
|
|
75
66
|
return {
|
|
76
|
-
title: title
|
|
77
|
-
artist: artist
|
|
78
|
-
album: album
|
|
79
|
-
comment: comment
|
|
80
|
-
genre: genre
|
|
81
|
-
year: year
|
|
82
|
-
track: track
|
|
67
|
+
title: tagWrapper.title(),
|
|
68
|
+
artist: tagWrapper.artist(),
|
|
69
|
+
album: tagWrapper.album(),
|
|
70
|
+
comment: tagWrapper.comment(),
|
|
71
|
+
genre: tagWrapper.genre(),
|
|
72
|
+
year: tagWrapper.year(),
|
|
73
|
+
track: tagWrapper.track(),
|
|
74
|
+
|
|
75
|
+
setTitle: (value: string) => tagWrapper.setTitle(value),
|
|
76
|
+
setArtist: (value: string) => tagWrapper.setArtist(value),
|
|
77
|
+
setAlbum: (value: string) => tagWrapper.setAlbum(value),
|
|
78
|
+
setComment: (value: string) => tagWrapper.setComment(value),
|
|
79
|
+
setGenre: (value: string) => tagWrapper.setGenre(value),
|
|
80
|
+
setYear: (value: number) => tagWrapper.setYear(value),
|
|
81
|
+
setTrack: (value: number) => tagWrapper.setTrack(value),
|
|
83
82
|
};
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
/**
|
|
87
|
-
* Get audio properties
|
|
86
|
+
* Get audio properties
|
|
88
87
|
*/
|
|
89
88
|
audioProperties(): AudioProperties | null {
|
|
90
|
-
if (this.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
channels,
|
|
106
|
-
format: this.format(),
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Set the title tag
|
|
112
|
-
*/
|
|
113
|
-
setTitle(title: string): void {
|
|
114
|
-
if (this.tagPtr === 0) return;
|
|
115
|
-
const titlePtr = jsToCString(this.module, title);
|
|
116
|
-
this.module._taglib_tag_set_title(this.tagPtr, titlePtr);
|
|
117
|
-
this.module._free(titlePtr);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Set the artist tag
|
|
122
|
-
*/
|
|
123
|
-
setArtist(artist: string): void {
|
|
124
|
-
if (this.tagPtr === 0) return;
|
|
125
|
-
const artistPtr = jsToCString(this.module, artist);
|
|
126
|
-
this.module._taglib_tag_set_artist(this.tagPtr, artistPtr);
|
|
127
|
-
this.module._free(artistPtr);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Set the album tag
|
|
132
|
-
*/
|
|
133
|
-
setAlbum(album: string): void {
|
|
134
|
-
if (this.tagPtr === 0) return;
|
|
135
|
-
const albumPtr = jsToCString(this.module, album);
|
|
136
|
-
this.module._taglib_tag_set_album(this.tagPtr, albumPtr);
|
|
137
|
-
this.module._free(albumPtr);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Set the comment tag
|
|
142
|
-
*/
|
|
143
|
-
setComment(comment: string): void {
|
|
144
|
-
if (this.tagPtr === 0) return;
|
|
145
|
-
const commentPtr = jsToCString(this.module, comment);
|
|
146
|
-
this.module._taglib_tag_set_comment(this.tagPtr, commentPtr);
|
|
147
|
-
this.module._free(commentPtr);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Set the genre tag
|
|
152
|
-
*/
|
|
153
|
-
setGenre(genre: string): void {
|
|
154
|
-
if (this.tagPtr === 0) return;
|
|
155
|
-
const genrePtr = jsToCString(this.module, genre);
|
|
156
|
-
this.module._taglib_tag_set_genre(this.tagPtr, genrePtr);
|
|
157
|
-
this.module._free(genrePtr);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Set the year tag
|
|
162
|
-
*/
|
|
163
|
-
setYear(year: number): void {
|
|
164
|
-
if (this.tagPtr === 0) return;
|
|
165
|
-
this.module._taglib_tag_set_year(this.tagPtr, year);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Set the track number tag
|
|
170
|
-
*/
|
|
171
|
-
setTrack(track: number): void {
|
|
172
|
-
if (this.tagPtr === 0) return;
|
|
173
|
-
this.module._taglib_tag_set_track(this.tagPtr, track);
|
|
89
|
+
if (!this.cachedAudioProperties) {
|
|
90
|
+
const propsWrapper = this.fileHandle.getAudioProperties();
|
|
91
|
+
if (!propsWrapper) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.cachedAudioProperties = {
|
|
96
|
+
length: propsWrapper.lengthInSeconds(),
|
|
97
|
+
bitrate: propsWrapper.bitrate(),
|
|
98
|
+
sampleRate: propsWrapper.sampleRate(),
|
|
99
|
+
channels: propsWrapper.channels(),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return this.cachedAudioProperties;
|
|
174
104
|
}
|
|
175
105
|
|
|
176
106
|
/**
|
|
177
107
|
* Get all properties as a PropertyMap
|
|
178
108
|
*/
|
|
179
109
|
properties(): PropertyMap {
|
|
180
|
-
const
|
|
181
|
-
|
|
110
|
+
const jsObj = this.fileHandle.getProperties();
|
|
111
|
+
const result: PropertyMap = {};
|
|
182
112
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return {};
|
|
113
|
+
// Convert from Emscripten val to plain object
|
|
114
|
+
const keys = Object.keys(jsObj);
|
|
115
|
+
for (const key of keys) {
|
|
116
|
+
result[key] = jsObj[key];
|
|
188
117
|
}
|
|
118
|
+
|
|
119
|
+
return result;
|
|
189
120
|
}
|
|
190
121
|
|
|
191
122
|
/**
|
|
192
123
|
* Set properties from a PropertyMap
|
|
193
124
|
*/
|
|
194
|
-
setProperties(properties: PropertyMap):
|
|
195
|
-
|
|
196
|
-
const jsonPtr = jsToCString(this.module, jsonStr);
|
|
197
|
-
try {
|
|
198
|
-
return this.module._taglib_file_set_properties_json(this.fileId, jsonPtr) !== 0;
|
|
199
|
-
} finally {
|
|
200
|
-
this.module._free(jsonPtr);
|
|
201
|
-
}
|
|
125
|
+
setProperties(properties: PropertyMap): void {
|
|
126
|
+
this.fileHandle.setProperties(properties);
|
|
202
127
|
}
|
|
203
128
|
|
|
204
129
|
/**
|
|
205
|
-
* Get a
|
|
130
|
+
* Get a single property value
|
|
206
131
|
*/
|
|
207
|
-
getProperty(key: string): string
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
const valuePtr = this.module._taglib_file_get_property(this.fileId, keyPtr);
|
|
211
|
-
if (valuePtr === 0) return undefined;
|
|
212
|
-
|
|
213
|
-
const value = cStringToJS(this.module, valuePtr);
|
|
214
|
-
return value ? [value] : undefined;
|
|
215
|
-
} finally {
|
|
216
|
-
this.module._free(keyPtr);
|
|
217
|
-
}
|
|
132
|
+
getProperty(key: string): string | undefined {
|
|
133
|
+
const value = this.fileHandle.getProperty(key);
|
|
134
|
+
return value === "" ? undefined : value;
|
|
218
135
|
}
|
|
219
136
|
|
|
220
137
|
/**
|
|
221
|
-
* Set a
|
|
138
|
+
* Set a single property value
|
|
222
139
|
*/
|
|
223
|
-
setProperty(key: string,
|
|
224
|
-
|
|
225
|
-
if (!value) return false;
|
|
226
|
-
|
|
227
|
-
const keyPtr = jsToCString(this.module, key);
|
|
228
|
-
const valuePtr = jsToCString(this.module, value);
|
|
229
|
-
try {
|
|
230
|
-
return this.module._taglib_file_set_property(this.fileId, keyPtr, valuePtr) !== 0;
|
|
231
|
-
} finally {
|
|
232
|
-
this.module._free(keyPtr);
|
|
233
|
-
this.module._free(valuePtr);
|
|
234
|
-
}
|
|
140
|
+
setProperty(key: string, value: string): void {
|
|
141
|
+
this.fileHandle.setProperty(key, value);
|
|
235
142
|
}
|
|
236
143
|
|
|
237
144
|
/**
|
|
238
145
|
* Check if this is an MP4 file
|
|
239
146
|
*/
|
|
240
147
|
isMP4(): boolean {
|
|
241
|
-
return this.
|
|
148
|
+
return this.fileHandle.isMP4();
|
|
242
149
|
}
|
|
243
150
|
|
|
244
151
|
/**
|
|
245
|
-
* Get MP4-specific item
|
|
152
|
+
* Get MP4-specific item
|
|
246
153
|
*/
|
|
247
154
|
getMP4Item(key: string): string | undefined {
|
|
248
|
-
if (!this.isMP4())
|
|
249
|
-
|
|
250
|
-
const keyPtr = jsToCString(this.module, key);
|
|
251
|
-
try {
|
|
252
|
-
const valuePtr = this.module._taglib_mp4_get_item(this.fileId, keyPtr);
|
|
253
|
-
if (valuePtr === 0) return undefined;
|
|
254
|
-
|
|
255
|
-
return cStringToJS(this.module, valuePtr);
|
|
256
|
-
} finally {
|
|
257
|
-
this.module._free(keyPtr);
|
|
155
|
+
if (!this.isMP4()) {
|
|
156
|
+
throw new Error("Not an MP4 file");
|
|
258
157
|
}
|
|
158
|
+
const value = this.fileHandle.getMP4Item(key);
|
|
159
|
+
return value === "" ? undefined : value;
|
|
259
160
|
}
|
|
260
161
|
|
|
261
162
|
/**
|
|
262
|
-
* Set MP4-specific item
|
|
163
|
+
* Set MP4-specific item
|
|
263
164
|
*/
|
|
264
|
-
setMP4Item(key: string, value: string):
|
|
265
|
-
if (!this.isMP4())
|
|
266
|
-
|
|
267
|
-
const keyPtr = jsToCString(this.module, key);
|
|
268
|
-
const valuePtr = jsToCString(this.module, value);
|
|
269
|
-
try {
|
|
270
|
-
return this.module._taglib_mp4_set_item(this.fileId, keyPtr, valuePtr) !== 0;
|
|
271
|
-
} finally {
|
|
272
|
-
this.module._free(keyPtr);
|
|
273
|
-
this.module._free(valuePtr);
|
|
165
|
+
setMP4Item(key: string, value: string): void {
|
|
166
|
+
if (!this.isMP4()) {
|
|
167
|
+
throw new Error("Not an MP4 file");
|
|
274
168
|
}
|
|
169
|
+
this.fileHandle.setMP4Item(key, value);
|
|
275
170
|
}
|
|
276
171
|
|
|
277
172
|
/**
|
|
278
173
|
* Remove MP4-specific item
|
|
279
174
|
*/
|
|
280
|
-
removeMP4Item(key: string):
|
|
281
|
-
if (!this.isMP4())
|
|
282
|
-
|
|
283
|
-
const keyPtr = jsToCString(this.module, key);
|
|
284
|
-
try {
|
|
285
|
-
return this.module._taglib_mp4_remove_item(this.fileId, keyPtr) !== 0;
|
|
286
|
-
} finally {
|
|
287
|
-
this.module._free(keyPtr);
|
|
175
|
+
removeMP4Item(key: string): void {
|
|
176
|
+
if (!this.isMP4()) {
|
|
177
|
+
throw new Error("Not an MP4 file");
|
|
288
178
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Get bitrate control mode (MP4/M4A specific)
|
|
293
|
-
* Reads from the 'acbf' atom
|
|
294
|
-
*/
|
|
295
|
-
getBitrateControlMode(): BitrateControlMode | undefined {
|
|
296
|
-
if (!this.isMP4()) return undefined;
|
|
297
|
-
|
|
298
|
-
const value = this.getMP4Item("acbf");
|
|
299
|
-
if (!value) return undefined;
|
|
300
|
-
|
|
301
|
-
const numValue = parseInt(value, 10);
|
|
302
|
-
if (isNaN(numValue) || numValue < 0 || numValue > 3) return undefined;
|
|
303
|
-
|
|
304
|
-
return BITRATE_CONTROL_MODE_NAMES[numValue];
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Set bitrate control mode (MP4/M4A specific)
|
|
309
|
-
* Writes to the 'acbf' atom
|
|
310
|
-
*/
|
|
311
|
-
setBitrateControlMode(mode: BitrateControlMode): boolean {
|
|
312
|
-
if (!this.isMP4()) return false;
|
|
313
|
-
|
|
314
|
-
const numValue = BITRATE_CONTROL_MODE_VALUES[mode];
|
|
315
|
-
if (numValue === undefined) return false;
|
|
316
|
-
|
|
317
|
-
return this.setMP4Item("acbf", numValue.toString());
|
|
179
|
+
this.fileHandle.removeMP4Item(key);
|
|
318
180
|
}
|
|
319
181
|
|
|
320
182
|
/**
|
|
321
183
|
* Save changes to the file
|
|
322
184
|
*/
|
|
323
185
|
save(): boolean {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
*/
|
|
330
|
-
extendedTag(): ExtendedTag {
|
|
331
|
-
// Get basic tags first
|
|
332
|
-
const basicTag = this.tag();
|
|
333
|
-
|
|
334
|
-
// TODO: Implement automatic tag mapping reading via PropertyMap
|
|
335
|
-
// For now, return basic tags with placeholder for advanced fields
|
|
336
|
-
return {
|
|
337
|
-
...basicTag,
|
|
338
|
-
// Advanced fields would be populated by PropertyMap reading
|
|
339
|
-
acoustidFingerprint: undefined,
|
|
340
|
-
acoustidId: undefined,
|
|
341
|
-
musicbrainzTrackId: undefined,
|
|
342
|
-
musicbrainzReleaseId: undefined,
|
|
343
|
-
musicbrainzArtistId: undefined,
|
|
344
|
-
musicbrainzReleaseGroupId: undefined,
|
|
345
|
-
albumArtist: undefined,
|
|
346
|
-
composer: undefined,
|
|
347
|
-
discNumber: undefined,
|
|
348
|
-
totalTracks: undefined,
|
|
349
|
-
totalDiscs: undefined,
|
|
350
|
-
bpm: undefined,
|
|
351
|
-
compilation: undefined,
|
|
352
|
-
titleSort: undefined,
|
|
353
|
-
artistSort: undefined,
|
|
354
|
-
albumSort: undefined,
|
|
355
|
-
// ReplayGain fields
|
|
356
|
-
replayGainTrackGain: undefined,
|
|
357
|
-
replayGainTrackPeak: undefined,
|
|
358
|
-
replayGainAlbumGain: undefined,
|
|
359
|
-
replayGainAlbumPeak: undefined,
|
|
360
|
-
// Apple Sound Check
|
|
361
|
-
appleSoundCheck: undefined,
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Set extended metadata using format-agnostic field names
|
|
367
|
-
* The library automatically maps fields to the correct format-specific storage
|
|
368
|
-
*/
|
|
369
|
-
setExtendedTag(tag: Partial<ExtendedTag>): void {
|
|
370
|
-
// Set basic tags using existing methods
|
|
371
|
-
if (tag.title !== undefined) this.setTitle(tag.title);
|
|
372
|
-
if (tag.artist !== undefined) this.setArtist(tag.artist);
|
|
373
|
-
if (tag.album !== undefined) this.setAlbum(tag.album);
|
|
374
|
-
if (tag.comment !== undefined) this.setComment(tag.comment);
|
|
375
|
-
if (tag.genre !== undefined) this.setGenre(tag.genre);
|
|
376
|
-
if (tag.year !== undefined) this.setYear(tag.year);
|
|
377
|
-
if (tag.track !== undefined) this.setTrack(tag.track);
|
|
378
|
-
|
|
379
|
-
// TODO: Implement automatic tag mapping writing via PropertyMap
|
|
380
|
-
// For fields like acoustidFingerprint, acoustidId, etc.
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Set AcoustID fingerprint (format-agnostic)
|
|
385
|
-
* Automatically stores in the correct location for each format:
|
|
386
|
-
* - MP3: TXXX frame with "Acoustid Fingerprint" description
|
|
387
|
-
* - FLAC/OGG: ACOUSTID_FINGERPRINT Vorbis comment
|
|
388
|
-
* - MP4: ----:com.apple.iTunes:Acoustid Fingerprint atom
|
|
389
|
-
*/
|
|
390
|
-
setAcoustidFingerprint(fingerprint: string): void {
|
|
391
|
-
// TODO: Implement format-specific storage
|
|
392
|
-
console.warn(
|
|
393
|
-
"setAcoustidFingerprint: Advanced metadata not yet implemented",
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Get AcoustID fingerprint (format-agnostic)
|
|
399
|
-
*/
|
|
400
|
-
getAcoustidFingerprint(): string | undefined {
|
|
401
|
-
// TODO: Implement format-specific reading
|
|
402
|
-
return undefined;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Set AcoustID UUID (format-agnostic)
|
|
407
|
-
* Automatically stores in the correct location for each format:
|
|
408
|
-
* - MP3: TXXX frame with "Acoustid Id" description
|
|
409
|
-
* - FLAC/OGG: ACOUSTID_ID Vorbis comment
|
|
410
|
-
* - MP4: ----:com.apple.iTunes:Acoustid Id atom
|
|
411
|
-
*/
|
|
412
|
-
setAcoustidId(id: string): void {
|
|
413
|
-
// TODO: Implement format-specific storage
|
|
414
|
-
console.warn("setAcoustidId: Advanced metadata not yet implemented");
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Get AcoustID UUID (format-agnostic)
|
|
419
|
-
*/
|
|
420
|
-
getAcoustidId(): string | undefined {
|
|
421
|
-
// TODO: Implement format-specific reading
|
|
422
|
-
return undefined;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* Set MusicBrainz Track ID (format-agnostic)
|
|
427
|
-
*/
|
|
428
|
-
setMusicBrainzTrackId(id: string): void {
|
|
429
|
-
// TODO: Implement format-specific storage
|
|
430
|
-
console.warn(
|
|
431
|
-
"setMusicBrainzTrackId: Advanced metadata not yet implemented",
|
|
432
|
-
);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Get MusicBrainz Track ID (format-agnostic)
|
|
437
|
-
*/
|
|
438
|
-
getMusicBrainzTrackId(): string | undefined {
|
|
439
|
-
// TODO: Implement format-specific reading
|
|
440
|
-
return undefined;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Set ReplayGain track gain (format-agnostic)
|
|
445
|
-
* Automatically stores in the correct location for each format:
|
|
446
|
-
* - MP3: TXXX frame with "ReplayGain_Track_Gain" description
|
|
447
|
-
* - FLAC/OGG: REPLAYGAIN_TRACK_GAIN Vorbis comment
|
|
448
|
-
* - MP4: ----:com.apple.iTunes:replaygain_track_gain atom
|
|
449
|
-
*/
|
|
450
|
-
setReplayGainTrackGain(gain: string): void {
|
|
451
|
-
// TODO: Implement format-specific storage
|
|
452
|
-
console.warn(
|
|
453
|
-
"setReplayGainTrackGain: Advanced metadata not yet implemented",
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Get ReplayGain track gain (format-agnostic)
|
|
459
|
-
*/
|
|
460
|
-
getReplayGainTrackGain(): string | undefined {
|
|
461
|
-
// TODO: Implement format-specific reading
|
|
462
|
-
return undefined;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Set ReplayGain track peak (format-agnostic)
|
|
467
|
-
*/
|
|
468
|
-
setReplayGainTrackPeak(peak: string): void {
|
|
469
|
-
// TODO: Implement format-specific storage
|
|
470
|
-
console.warn(
|
|
471
|
-
"setReplayGainTrackPeak: Advanced metadata not yet implemented",
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Get ReplayGain track peak (format-agnostic)
|
|
477
|
-
*/
|
|
478
|
-
getReplayGainTrackPeak(): string | undefined {
|
|
479
|
-
// TODO: Implement format-specific reading
|
|
480
|
-
return undefined;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Set ReplayGain album gain (format-agnostic)
|
|
485
|
-
*/
|
|
486
|
-
setReplayGainAlbumGain(gain: string): void {
|
|
487
|
-
// TODO: Implement format-specific storage
|
|
488
|
-
console.warn(
|
|
489
|
-
"setReplayGainAlbumGain: Advanced metadata not yet implemented",
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Get ReplayGain album gain (format-agnostic)
|
|
495
|
-
*/
|
|
496
|
-
getReplayGainAlbumGain(): string | undefined {
|
|
497
|
-
// TODO: Implement format-specific reading
|
|
498
|
-
return undefined;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Set ReplayGain album peak (format-agnostic)
|
|
503
|
-
*/
|
|
504
|
-
setReplayGainAlbumPeak(peak: string): void {
|
|
505
|
-
// TODO: Implement format-specific storage
|
|
506
|
-
console.warn(
|
|
507
|
-
"setReplayGainAlbumPeak: Advanced metadata not yet implemented",
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Get ReplayGain album peak (format-agnostic)
|
|
513
|
-
*/
|
|
514
|
-
getReplayGainAlbumPeak(): string | undefined {
|
|
515
|
-
// TODO: Implement format-specific reading
|
|
516
|
-
return undefined;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
/**
|
|
520
|
-
* Set Apple Sound Check normalization data (format-agnostic)
|
|
521
|
-
* Automatically stores in the correct location for each format:
|
|
522
|
-
* - MP3: TXXX frame with "iTunNORM" description
|
|
523
|
-
* - FLAC/OGG: ITUNNORM Vorbis comment
|
|
524
|
-
* - MP4: ----:com.apple.iTunes:iTunNORM atom
|
|
525
|
-
*/
|
|
526
|
-
setAppleSoundCheck(iTunNORM: string): void {
|
|
527
|
-
// TODO: Implement format-specific storage
|
|
528
|
-
console.warn("setAppleSoundCheck: Advanced metadata not yet implemented");
|
|
186
|
+
// Clear caches since values may have changed
|
|
187
|
+
this.cachedTag = null;
|
|
188
|
+
this.cachedAudioProperties = null;
|
|
189
|
+
|
|
190
|
+
return this.fileHandle.save();
|
|
529
191
|
}
|
|
530
192
|
|
|
531
193
|
/**
|
|
532
|
-
*
|
|
194
|
+
* Check if the file is valid
|
|
533
195
|
*/
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
return undefined;
|
|
196
|
+
isValid(): boolean {
|
|
197
|
+
return this.fileHandle.isValid();
|
|
537
198
|
}
|
|
538
199
|
|
|
539
200
|
/**
|
|
540
|
-
*
|
|
201
|
+
* Free resources
|
|
541
202
|
*/
|
|
542
203
|
dispose(): void {
|
|
543
|
-
if (this.
|
|
544
|
-
|
|
545
|
-
|
|
204
|
+
if (this.fileHandle) {
|
|
205
|
+
// Embind will handle cleanup when the object goes out of scope
|
|
206
|
+
// But we can help by clearing our references
|
|
207
|
+
this.fileHandle = null;
|
|
208
|
+
this.cachedTag = null;
|
|
209
|
+
this.cachedAudioProperties = null;
|
|
546
210
|
}
|
|
547
211
|
}
|
|
548
212
|
}
|
|
549
213
|
|
|
550
214
|
/**
|
|
551
|
-
* Main TagLib
|
|
215
|
+
* Main TagLib interface using Embind
|
|
552
216
|
*/
|
|
553
217
|
export class TagLib {
|
|
554
218
|
private module: TagLibModule;
|
|
555
219
|
|
|
556
|
-
|
|
557
|
-
this.module = module;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* Initialize TagLib with optional configuration
|
|
562
|
-
*/
|
|
563
|
-
static async initialize(config?: TagLibConfig): Promise<TagLib> {
|
|
564
|
-
const module = await loadTagLibModule(config);
|
|
565
|
-
return new TagLib(module);
|
|
220
|
+
constructor(module: WasmModule) {
|
|
221
|
+
this.module = module as TagLibModule;
|
|
566
222
|
}
|
|
567
223
|
|
|
568
224
|
/**
|
|
569
|
-
* Open
|
|
225
|
+
* Open a file from a buffer
|
|
570
226
|
*/
|
|
571
|
-
openFile(buffer:
|
|
572
|
-
if
|
|
573
|
-
|
|
227
|
+
async openFile(buffer: ArrayBuffer): Promise<AudioFile> {
|
|
228
|
+
// Check if Embind is available
|
|
229
|
+
if (!this.module.createFileHandle) {
|
|
230
|
+
throw new Error("TagLib module not properly initialized - createFileHandle not found");
|
|
574
231
|
}
|
|
575
|
-
|
|
576
|
-
//
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
if (
|
|
585
|
-
|
|
586
|
-
console.log(
|
|
587
|
-
`DEBUG: File creation failed, not freeing memory at ${dataPtr}`,
|
|
588
|
-
);
|
|
589
|
-
throw new Error(
|
|
590
|
-
"Failed to open audio file - invalid format or corrupted data",
|
|
591
|
-
);
|
|
232
|
+
|
|
233
|
+
// Convert ArrayBuffer to Uint8Array for Embind
|
|
234
|
+
const uint8Array = new Uint8Array(buffer);
|
|
235
|
+
|
|
236
|
+
// Create a new FileHandle
|
|
237
|
+
const fileHandle = this.module.createFileHandle();
|
|
238
|
+
|
|
239
|
+
// Load the buffer - Embind should handle Uint8Array conversion
|
|
240
|
+
const success = fileHandle.loadFromBuffer(uint8Array);
|
|
241
|
+
if (!success) {
|
|
242
|
+
throw new Error("Failed to load file from buffer");
|
|
592
243
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
this.module._free(dataPtr);
|
|
596
|
-
|
|
597
|
-
return new AudioFile(this.module, fileId);
|
|
244
|
+
|
|
245
|
+
return new AudioFileImpl(this.module, fileHandle);
|
|
598
246
|
}
|
|
599
247
|
|
|
600
248
|
/**
|
|
601
|
-
* Get
|
|
249
|
+
* Get version information
|
|
602
250
|
*/
|
|
603
|
-
|
|
604
|
-
return
|
|
251
|
+
version(): string {
|
|
252
|
+
return "2.1.0"; // TagLib version we're using
|
|
605
253
|
}
|
|
606
254
|
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Create a TagLib instance
|
|
258
|
+
*/
|
|
259
|
+
export async function createTagLib(module: WasmModule): Promise<TagLib> {
|
|
260
|
+
return new TagLib(module);
|
|
261
|
+
}
|