Add support for ID3v2.4 multi-value tags

Add support for multi-value tags based on null terminators.

These are specific to ID3v2.4, but is backwards compatible with
ID3v2.3, so no version checks are needed.
parent ab4d37f4
...@@ -29,8 +29,10 @@ import java.io.UnsupportedEncodingException; ...@@ -29,8 +29,10 @@ import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import org.w3c.dom.Text;
/** Decodes ID3 tags. */ /** Decodes ID3 tags. */
public final class Id3Decoder extends SimpleMetadataDecoder { public final class Id3Decoder extends SimpleMetadataDecoder {
...@@ -458,11 +460,23 @@ public final class Id3Decoder extends SimpleMetadataDecoder { ...@@ -458,11 +460,23 @@ public final class Id3Decoder extends SimpleMetadataDecoder {
int descriptionEndIndex = indexOfEos(data, 0, encoding); int descriptionEndIndex = indexOfEos(data, 0, encoding);
String description = new String(data, 0, descriptionEndIndex, charset); String description = new String(data, 0, descriptionEndIndex, charset);
// Text information frames can contain multiple values delimited by a null terminator.
// Thus, we after each "end of stream" marker we actually need to keep looking for more
// data, at least until the index is equal to the data length.
ArrayList<String> values = new ArrayList<>();
int valueStartIndex = descriptionEndIndex + delimiterLength(encoding); int valueStartIndex = descriptionEndIndex + delimiterLength(encoding);
int valueEndIndex = indexOfEos(data, valueStartIndex, encoding); int valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
String value = decodeStringIfValid(data, valueStartIndex, valueEndIndex, charset); while (valueStartIndex < valueEndIndex) {
String value = decodeStringIfValid(data, valueStartIndex, valueEndIndex, charset);
values.add(value);
valueStartIndex = valueEndIndex + delimiterLength(encoding);
valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
}
return new TextInformationFrame("TXXX", description, value); return new TextInformationFrame("TXXX", description, values.toArray(new String[0]));
} }
@Nullable @Nullable
...@@ -479,10 +493,22 @@ public final class Id3Decoder extends SimpleMetadataDecoder { ...@@ -479,10 +493,22 @@ public final class Id3Decoder extends SimpleMetadataDecoder {
byte[] data = new byte[frameSize - 1]; byte[] data = new byte[frameSize - 1];
id3Data.readBytes(data, 0, frameSize - 1); id3Data.readBytes(data, 0, frameSize - 1);
int valueEndIndex = indexOfEos(data, 0, encoding); // Text information frames can contain multiple values delimited by a null terminator.
String value = new String(data, 0, valueEndIndex, charset); // Thus, we after each "end of stream" marker we actually need to keep looking for more
// data, at least until the index is equal to the data length.
ArrayList<String> values = new ArrayList<>();
int valueStartIndex = 0;
int valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
while (valueStartIndex < valueEndIndex) {
String value = decodeStringIfValid(data, valueStartIndex, valueEndIndex, charset);
values.add(value);
valueStartIndex = valueEndIndex + delimiterLength(encoding);
valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
}
return new TextInformationFrame(id, null, value); return new TextInformationFrame(id, null, values.toArray(new String[0]));
} }
@Nullable @Nullable
......
...@@ -23,48 +23,71 @@ import androidx.annotation.Nullable; ...@@ -23,48 +23,71 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.MediaMetadata; import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
/** Text information ID3 frame. */ /** Text information ID3 frame. */
public final class TextInformationFrame extends Id3Frame { public final class TextInformationFrame extends Id3Frame {
private final static String MULTI_VALUE_DELIMITER = ", ";
@Nullable public final String description; @Nullable public final String description;
/** @deprecated Use {@code values} instead. */
@Deprecated
public final String value; public final String value;
public TextInformationFrame(String id, @Nullable String description, String value) { public final String[] values;
public TextInformationFrame(String id, @Nullable String description, String[] values) {
super(id); super(id);
if (values.length == 0) {
throw new IllegalArgumentException("A text information frame must have at least one value.");
}
this.description = description; this.description = description;
this.value = value; this.values = values;
this.value = values[0];
}
/** @deprecated Use {@code TextInformationFrame(String id, String description, String[] values} instead */
@Deprecated
public TextInformationFrame(String id, @Nullable String description, String value) {
this(id, description, new String[] {value } );
} }
/* package */ TextInformationFrame(Parcel in) { /* package */ TextInformationFrame(Parcel in) {
super(castNonNull(in.readString())); super(castNonNull(in.readString()));
description = in.readString(); description = in.readString();
value = castNonNull(in.readString()); values = new String[] {};
in.readStringArray(values);
this.value = values[0];
} }
@Override @Override
public void populateMediaMetadata(MediaMetadata.Builder builder) { public void populateMediaMetadata(MediaMetadata.Builder builder) {
// Depending on the context this frame is in, we either take the first value of a multi-value
// frame because multiple values make no sense, or we join the values together with a comma
// when multiple values do make sense.
switch (id) { switch (id) {
case "TT2": case "TT2":
case "TIT2": case "TIT2":
builder.setTitle(value); builder.setTitle(values[0]);
break; break;
case "TP1": case "TP1":
case "TPE1": case "TPE1":
builder.setArtist(value); builder.setArtist(String.join(MULTI_VALUE_DELIMITER, values));
break; break;
case "TP2": case "TP2":
case "TPE2": case "TPE2":
builder.setAlbumArtist(value); builder.setAlbumArtist(String.join(MULTI_VALUE_DELIMITER, values));
break; break;
case "TAL": case "TAL":
case "TALB": case "TALB":
builder.setAlbumTitle(value); builder.setAlbumTitle(values[0]);
break; break;
case "TRK": case "TRK":
case "TRCK": case "TRCK":
String[] trackNumbers = Util.split(value, "/"); String[] trackNumbers = Util.split(values[0], "/");
try { try {
int trackNumber = Integer.parseInt(trackNumbers[0]); int trackNumber = Integer.parseInt(trackNumbers[0]);
@Nullable @Nullable
...@@ -78,7 +101,7 @@ public final class TextInformationFrame extends Id3Frame { ...@@ -78,7 +101,7 @@ public final class TextInformationFrame extends Id3Frame {
case "TYE": case "TYE":
case "TYER": case "TYER":
try { try {
builder.setRecordingYear(Integer.parseInt(value)); builder.setRecordingYear(Integer.parseInt(values[0]));
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
// Do nothing, invalid input. // Do nothing, invalid input.
} }
...@@ -86,15 +109,16 @@ public final class TextInformationFrame extends Id3Frame { ...@@ -86,15 +109,16 @@ public final class TextInformationFrame extends Id3Frame {
case "TDA": case "TDA":
case "TDAT": case "TDAT":
try { try {
int month = Integer.parseInt(value.substring(2, 4)); String date = values[0];
int day = Integer.parseInt(value.substring(0, 2)); int month = Integer.parseInt(date.substring(2, 4));
int day = Integer.parseInt(date.substring(0, 2));
builder.setRecordingMonth(month).setRecordingDay(day); builder.setRecordingMonth(month).setRecordingDay(day);
} catch (NumberFormatException | StringIndexOutOfBoundsException e) { } catch (NumberFormatException | StringIndexOutOfBoundsException e) {
// Do nothing, invalid input. // Do nothing, invalid input.
} }
break; break;
case "TDRC": case "TDRC":
List<Integer> recordingDate = parseId3v2point4TimestampFrameForDate(value); List<Integer> recordingDate = parseId3v2point4TimestampFrameForDate(values[0]);
switch (recordingDate.size()) { switch (recordingDate.size()) {
case 3: case 3:
builder.setRecordingDay(recordingDate.get(2)); builder.setRecordingDay(recordingDate.get(2));
...@@ -112,7 +136,7 @@ public final class TextInformationFrame extends Id3Frame { ...@@ -112,7 +136,7 @@ public final class TextInformationFrame extends Id3Frame {
} }
break; break;
case "TDRL": case "TDRL":
List<Integer> releaseDate = parseId3v2point4TimestampFrameForDate(value); List<Integer> releaseDate = parseId3v2point4TimestampFrameForDate(values[0]);
switch (releaseDate.size()) { switch (releaseDate.size()) {
case 3: case 3:
builder.setReleaseDay(releaseDate.get(2)); builder.setReleaseDay(releaseDate.get(2));
...@@ -131,15 +155,15 @@ public final class TextInformationFrame extends Id3Frame { ...@@ -131,15 +155,15 @@ public final class TextInformationFrame extends Id3Frame {
break; break;
case "TCM": case "TCM":
case "TCOM": case "TCOM":
builder.setComposer(value); builder.setComposer(String.join(MULTI_VALUE_DELIMITER, values));
break; break;
case "TP3": case "TP3":
case "TPE3": case "TPE3":
builder.setConductor(value); builder.setConductor(String.join(MULTI_VALUE_DELIMITER, values));
break; break;
case "TXT": case "TXT":
case "TEXT": case "TEXT":
builder.setWriter(value); builder.setWriter(String.join(MULTI_VALUE_DELIMITER, values));
break; break;
default: default:
break; break;
...@@ -157,7 +181,7 @@ public final class TextInformationFrame extends Id3Frame { ...@@ -157,7 +181,7 @@ public final class TextInformationFrame extends Id3Frame {
TextInformationFrame other = (TextInformationFrame) obj; TextInformationFrame other = (TextInformationFrame) obj;
return Util.areEqual(id, other.id) return Util.areEqual(id, other.id)
&& Util.areEqual(description, other.description) && Util.areEqual(description, other.description)
&& Util.areEqual(value, other.value); && Util.areEqual(values, other.values);
} }
@Override @Override
...@@ -165,13 +189,13 @@ public final class TextInformationFrame extends Id3Frame { ...@@ -165,13 +189,13 @@ public final class TextInformationFrame extends Id3Frame {
int result = 17; int result = 17;
result = 31 * result + id.hashCode(); result = 31 * result + id.hashCode();
result = 31 * result + (description != null ? description.hashCode() : 0); result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + (value != null ? value.hashCode() : 0); result = 31 * result + Arrays.hashCode(values);
return result; return result;
} }
@Override @Override
public String toString() { public String toString() {
return id + ": description=" + description + ": value=" + value; return id + ": description=" + description + ": values=" + String.join(MULTI_VALUE_DELIMITER, values);
} }
// Parcelable implementation. // Parcelable implementation.
...@@ -180,7 +204,7 @@ public final class TextInformationFrame extends Id3Frame { ...@@ -180,7 +204,7 @@ public final class TextInformationFrame extends Id3Frame {
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id); dest.writeString(id);
dest.writeString(description); dest.writeString(description);
dest.writeString(value); dest.writeStringArray(values);
} }
public static final Parcelable.Creator<TextInformationFrame> CREATOR = public static final Parcelable.Creator<TextInformationFrame> CREATOR =
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment