TagLib Java1.3.0GitHub ↗

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.

Java
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

Java
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.

Java
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.