Common fields, multiple values, and replacement semantics
Text properties and editing
Native tag formats disagree about field names and representations. An MP3 title is usually an ID3v2
TIT2 frame, a Vorbis comment uses TITLE, and an MP4 uses ©nam. The property
interface presents these as TITLE. This follows TagLib’s property mapping model; it does not make all native
formats equally expressive.
Common fields and multiple values
metadata.tag() is a convenience view of title, artist, album, comment, genre, year, and track. Use
metadata.properties() when you need full date text, multiple values, or fields outside that small
common set.
var properties = TagLib.read(Path.of("song.flac")).properties();
String title = properties.first(PropertyKey.TITLE);
java.util.List<String> artists = properties.get(PropertyKey.ARTIST);
String date = properties.first(PropertyKey.DATE);
String catalogNumber = properties.first("CATALOGNUMBER");
get returns an empty list for an absent key; first returns an empty string for an
absent key or empty value list. Use containsKey if presence matters. Property names are ASCII
case-insensitive and stored in uppercase. Values keep their order and are not case-normalized.
PropertyKey supplies known names, not a promise of support in every format. Its key()
method gives the equivalent string: ALBUM_ARTIST is ALBUMARTIST,
TRACK_NUMBER is TRACKNUMBER, and TRACKER_NAME is TRACKERNAME.
Custom string keys use the same namespace.
Dates and track numbers
DATE is text, so a year and a full date can be retained where the tag format supports them. The
common year() accessor cannot represent the rest of a date. TRACKNUMBER and DISCNUMBER
can contain number/total where supported; TRACKTOTAL and DISCTOTAL are
separate properties. Check the stored representation after saving if your application depends on a particular
form.
Two artists should be submitted as two values rather than as an application-specific string joined with a comma. Some formats or fields still impose a single-value restriction. A recognized property name does not bypass encoding, length, or cardinality constraints.
Edit selected fields
var edit = TagLib.edit(Path.of("song.flac"))
.set(PropertyKey.TITLE, "New title")
.set(PropertyKey.ARTIST, java.util.List.of("Artist A", "Artist B"))
.remove(PropertyKey.COMMENT)
.build();
edit.save();
Building an edit does not open the file. build() captures an immutable request; save()
applies it to the file’s then-current metadata. Saving the request again reads the current file again. The last
operation for a text key wins. Supplying no values to set is invalid; use remove to
remove a property. An empty string is a submitted value, subject to the format’s rules.
The implementation merges text changes into a complete map and uses the existing replacement writer. Unspecified properties are retained through that writer, but can still be normalized. Tag versions and physical layout can change. Rejected text values prevent the fluent edit from saving; an I/O failure once saving begins can still leave partial changes.
Replace the complete text map
TagLib.write is appropriate when you have the entire desired text map. Omitted text keys are
removed. To start with the existing values, use toBuilder(); starting with PropertyMap.builder()
creates a replacement from scratch.
var path = Path.of("song.flac");
var replacement = TagLib.read(path).properties().toBuilder()
.put(PropertyKey.TITLE, "New title")
.add(PropertyKey.ARTIST, "Guest artist")
.remove(PropertyKey.COMMENT)
.build();
var result = TagLib.write(path, replacement);
if (!result.rejected().isEmpty()) {
System.err.println("Not saved: " + result.rejected().asMap());
}
In a property builder, put replaces a key’s values and add appends values. The built
map copies its inputs. A separate read followed by a write uses the snapshot you read; it does not incorporate
text changes another writer made between those calls. Use the fluent editor for a small set of changes to current
properties.
A complete-map write can save accepted values even when others are rejected. Inspect
WriteResult.rejected(). The applied() map reports accepted input, not a fresh read of
stored metadata. Read again if normalization or field mapping matters to your application.
Metadata outside the property map
Not all native data can be represented as text. unsupportedData() contains format-specific
identifiers for opaque metadata; it does not mean the file is unreadable. This is separate from the rejected
values returned by a write. An ordinary text replacement preserves opaque data according to the format writer’s
rules.
Removing a text key does not remove its opaque counterparts. AudioFile.removeUnsupportedProperties
explicitly requests removal of the supplied opaque identifiers. Treat this as a deletion operation: inspect the
identifiers first and use it only when you intend to discard that native metadata.
What saving preserves
TagLib.write(path, properties) keeps existing artwork; its overload with a picture list replaces all
artwork. The audio payload and opaque data are preserved according to the writer’s format rules. This is a
semantic preservation guarantee, not a byte-for-byte copy of metadata. If you need a specific ID3 version or
tag-family policy, use the format-specific save options.