Skip to content

Commit ec7ed7a

Browse files
committed
support partial writes
1 parent 963eff2 commit ec7ed7a

File tree

4 files changed

+127
-20
lines changed

4 files changed

+127
-20
lines changed

taglib.cpp

+23-9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ char *to_char_array(const TagLib::String &s) {
99
const std::string str = s.to8Bit(true);
1010
return ::strdup(str.c_str());
1111
}
12+
1213
TagLib::String to_string(const char *s) {
1314
return TagLib::String(s, TagLib::String::UTF8);
1415
}
@@ -37,7 +38,7 @@ taglib_file_tags(const char *filename) {
3738
for (const auto &kvs : properties)
3839
for (const auto &v : kvs.second) {
3940
TagLib::String row = kvs.first + "\t" + v;
40-
tags[i] = stringToCharArray(row);
41+
tags[i] = to_char_array(row);
4142
i++;
4243
}
4344
tags[len] = nullptr;
@@ -46,7 +47,7 @@ taglib_file_tags(const char *filename) {
4647
}
4748

4849
__attribute__((export_name("taglib_file_write_tags"))) bool
49-
taglib_file_write_tags(const char *filename, const char **tags) {
50+
taglib_file_write_tags(const char *filename, const char **tags, bool replace) {
5051
if (!filename || !tags)
5152
return false;
5253

@@ -55,18 +56,31 @@ taglib_file_write_tags(const char *filename, const char **tags) {
5556
return false;
5657

5758
TagLib::PropertyMap properties;
58-
for (size_t i = 0; tags[i] != NULL; i++) {
59-
TagLib::String row = to_string(tags[i]);
60-
if (auto ti = row.find("\t"); ti >= 0) {
59+
for (size_t i = 0; tags[i] != nullptr; i++) {
60+
TagLib::String row(tags[i], TagLib::String::UTF8);
61+
if (auto ti = row.find("\t"); ti != -1) {
6162
TagLib::String key(row.substr(0, ti));
62-
TagLib::StringList value(row.substr(ti + 1));
63-
properties.insert(key, value);
63+
if (auto v = row.substr(ti + 1); !v.isEmpty()) {
64+
properties.insert(key, TagLib::StringList(v));
65+
} else {
66+
properties.insert(key, TagLib::StringList());
67+
}
6468
}
6569
}
6670

67-
if (auto rejected = file.setProperties(properties); rejected.size() > 0)
68-
return 0;
71+
if (replace) {
72+
if (auto rejected = file.setProperties(properties); !rejected.isEmpty())
73+
return 0;
74+
return file.save();
75+
}
6976

77+
auto existing = file.properties();
78+
existing.erase(properties); // wipe the keys we're sending
79+
existing.merge(properties); // merge them in
80+
existing.removeEmpty();
81+
82+
if (auto rejected = file.setProperties(existing); !rejected.isEmpty())
83+
return 0;
7084
return file.save();
7185
}
7286

taglib.go

+22-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ var ErrSavingFile = fmt.Errorf("can't save file")
3030

3131
// These constants define normalized tag keys used by TagLib's [property mapping].
3232
// When using [ReadTags], the library will map format-specific metadata to these standardized keys.
33-
// Similarly, [WriteTags] will map these keys back to the appropriate format-specific fields.
33+
// Similarly, [WriteTags] and [ReplaceTags] will map these keys back to the appropriate format-specific fields.
3434
//
3535
// While these constants provide a consistent interface across different audio formats,
3636
// you can also use custom tag keys if the underlying format supports arbitrary tags.
@@ -221,8 +221,17 @@ func ReadProperties(path string) (Properties, error) {
221221
}, nil
222222
}
223223

224-
// WriteTags writes metadata tags to an audio file at the given path.
224+
// WriteTags writes the metadata key-values pairs to path, leaving other found keys untouched.
225225
func WriteTags(path string, tags map[string][]string) error {
226+
return writeTagsOpt(path, tags, false)
227+
}
228+
229+
// ReplaceTags replaces all tag metadata at path with the the metadata in thhe map.
230+
func ReplaceTags(path string, tags map[string][]string) error {
231+
return writeTagsOpt(path, tags, true)
232+
}
233+
234+
func writeTagsOpt(path string, tags map[string][]string, replace bool) error {
226235
var err error
227236
path, err = filepath.Abs(path)
228237
if err != nil {
@@ -238,13 +247,17 @@ func WriteTags(path string, tags map[string][]string) error {
238247

239248
var raw []string
240249
for k, vs := range tags {
250+
if len(vs) == 0 {
251+
raw = append(raw, k+"\t") // allow clearing
252+
continue
253+
}
241254
for _, v := range vs {
242255
raw = append(raw, k+"\t"+v)
243256
}
244257
}
245258

246259
var out bool
247-
if err := mod.call("taglib_file_write_tags", &out, wasmPath(path), raw); err != nil {
260+
if err := mod.call("taglib_file_write_tags", &out, wasmPath(path), raw, replace); err != nil {
248261
return fmt.Errorf("call: %w", err)
249262
}
250263
if !out {
@@ -353,6 +366,12 @@ func (m *module) call(name string, dest any, args ...any) error {
353366
params := make([]uint64, 0, len(args))
354367
for _, a := range args {
355368
switch a := a.(type) {
369+
case bool:
370+
if a {
371+
params = append(params, 1)
372+
} else {
373+
params = append(params, 0)
374+
}
356375
case int:
357376
params = append(params, uint64(a))
358377
case uint32:

taglib.wasm

200 Bytes
Binary file not shown.

taglib_test.go

+82-8
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ func TestClear(t *testing.T) {
3232
for _, path := range paths {
3333
t.Run(filepath.Base(path), func(t *testing.T) {
3434
// set some tags first
35-
err := taglib.WriteTags(path, map[string][]string{
35+
err := taglib.ReplaceTags(path, map[string][]string{
3636
"ARTIST": {"Example A"},
3737
"ALUMARTIST": {"Example"},
3838
})
3939
nilErr(t, err)
4040

4141
// then clear
42-
err = taglib.WriteTags(path, nil)
42+
err = taglib.ReplaceTags(path, nil)
4343
nilErr(t, err)
4444

4545
got, err := taglib.ReadTags(path)
@@ -94,20 +94,88 @@ func TestReadWrite(t *testing.T) {
9494
for _, path := range paths {
9595
for i, tags := range testTags {
9696
t.Run(fmt.Sprintf("%s_tags_%d", filepath.Base(path), i), func(t *testing.T) {
97-
err := taglib.WriteTags(path, tags)
97+
err := taglib.ReplaceTags(path, tags)
9898
nilErr(t, err)
9999

100100
got, err := taglib.ReadTags(path)
101101
nilErr(t, err)
102102

103-
if !maps.EqualFunc(got, tags, slices.Equal) {
104-
t.Fatalf("%v != %v", got, tags)
105-
}
103+
tagEq(t, got, tags)
106104
})
107105
}
108106
}
109107
}
110108

109+
func TestMergeWrite(t *testing.T) {
110+
t.Parallel()
111+
112+
paths := testPaths(t)
113+
114+
cmp := func(t *testing.T, path string, want map[string][]string) {
115+
t.Helper()
116+
tags, err := taglib.ReadTags(path)
117+
nilErr(t, err)
118+
tagEq(t, tags, want)
119+
}
120+
121+
for _, path := range paths {
122+
t.Run(filepath.Base(path), func(t *testing.T) {
123+
err := taglib.ReplaceTags(path, nil) // clear
124+
nilErr(t, err)
125+
126+
err = taglib.WriteTags(path, map[string][]string{
127+
"ONE": {"one"},
128+
})
129+
nilErr(t, err)
130+
cmp(t, path, map[string][]string{
131+
"ONE": {"one"},
132+
})
133+
134+
nilErr(t, err)
135+
err = taglib.WriteTags(path, map[string][]string{
136+
"TWO": {"two", "two!"},
137+
})
138+
nilErr(t, err)
139+
cmp(t, path, map[string][]string{
140+
"ONE": {"one"},
141+
"TWO": {"two", "two!"},
142+
})
143+
144+
err = taglib.WriteTags(path, map[string][]string{
145+
"THREE": {"three"},
146+
})
147+
nilErr(t, err)
148+
cmp(t, path, map[string][]string{
149+
"ONE": {"one"},
150+
"TWO": {"two", "two!"},
151+
"THREE": {"three"},
152+
})
153+
154+
// change prev
155+
err = taglib.WriteTags(path, map[string][]string{
156+
"ONE": {"one new"},
157+
})
158+
nilErr(t, err)
159+
cmp(t, path, map[string][]string{
160+
"ONE": {"one new"},
161+
"TWO": {"two", "two!"},
162+
"THREE": {"three"},
163+
})
164+
165+
// change prev
166+
err = taglib.WriteTags(path, map[string][]string{
167+
"ONE": {},
168+
"THREE": {"three new!"},
169+
})
170+
nilErr(t, err)
171+
cmp(t, path, map[string][]string{
172+
"TWO": {"two", "two!"},
173+
"THREE": {"three new!"},
174+
})
175+
})
176+
}
177+
}
178+
111179
func TestReadExistingUnicode(t *testing.T) {
112180
tags, err := taglib.ReadTags("testdata/normal.flac")
113181
nilErr(t, err)
@@ -208,14 +276,14 @@ func BenchmarkWrite(b *testing.B) {
208276
b.ResetTimer()
209277

210278
for range b.N {
211-
err := taglib.WriteTags(path, bigTags)
279+
err := taglib.ReplaceTags(path, bigTags)
212280
nilErr(b, err)
213281
}
214282
}
215283

216284
func BenchmarkRead(b *testing.B) {
217285
path := tmpf(b, egFLAC, "eg.flac")
218-
err := taglib.WriteTags(path, bigTags)
286+
err := taglib.ReplaceTags(path, bigTags)
219287
nilErr(b, err)
220288
b.ResetTimer()
221289

@@ -267,6 +335,12 @@ func eq[T comparable](t testing.TB, a, b T) {
267335
t.Fatalf("%v != %v", a, b)
268336
}
269337
}
338+
func tagEq(t testing.TB, a, b map[string][]string) {
339+
if !maps.EqualFunc(a, b, slices.Equal) {
340+
t.Helper()
341+
t.Fatalf("%q != %q", a, b)
342+
}
343+
}
270344

271345
func checkMem(t testing.TB) {
272346
stop := make(chan struct{})

0 commit comments

Comments
 (0)