Rework ID3v2 text frame values interface

Rework the interface for ID3v2 text frames by:
- Switching from a mutable array to ImmutableList
- Never leaving the array empty, instead filling it with "" if there
are no values

This makes the surface safer and more in line with the rest of the
module.
This commit is contained in:
Alexander Capehart 2022-11-21 11:33:48 -07:00
parent c96b55029d
commit 157f1f0787
No known key found for this signature in database
GPG Key ID: 37DBE3621FE9AD47
5 changed files with 71 additions and 53 deletions

View File

@ -811,7 +811,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
if (entry instanceof TextInformationFrame) { if (entry instanceof TextInformationFrame) {
TextInformationFrame textFrame = (TextInformationFrame) entry; TextInformationFrame textFrame = (TextInformationFrame) entry;
if ("TXXX".equals(textFrame.id)) { if ("TXXX".equals(textFrame.id)) {
streamPlayer.triggerUserTextReceived(textFrame.values[0]); streamPlayer.triggerUserTextReceived(textFrame.values.get(0));
} }
} else if (entry instanceof EventMessage) { } else if (entry instanceof EventMessage) {
EventMessage eventMessage = (EventMessage) entry; EventMessage eventMessage = (EventMessage) entry;

View File

@ -592,7 +592,7 @@ public final class Mp3Extractor implements Extractor {
Metadata.Entry entry = metadata.get(i); Metadata.Entry entry = metadata.get(i);
if (entry instanceof TextInformationFrame if (entry instanceof TextInformationFrame
&& ((TextInformationFrame) entry).id.equals("TLEN")) { && ((TextInformationFrame) entry).id.equals("TLEN")) {
return Util.msToUs(Long.parseLong(((TextInformationFrame) entry).values[0])); return Util.msToUs(Long.parseLong(((TextInformationFrame) entry).values.get(0)));
} }
} }
} }

View File

@ -23,33 +23,32 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; 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 com.google.common.collect.ImmutableList;
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 Use {@code values[0]} instead. */
@Deprecated @Deprecated
public final String value; public final String value;
@NonNull @NonNull
public final String[] values; public final ImmutableList<String> values;
public TextInformationFrame(String id, @Nullable String description, @NonNull String[] values) { public TextInformationFrame(String id, @Nullable String description, @NonNull String[] values) {
super(id); super(id);
this.description = description; this.description = description;
this.values = values;
if (values.length > 0) { if( values.length > 0 ) {
this.value = values[0]; this.values = ImmutableList.copyOf(values);
} else { } else {
this.value = null; this.values = ImmutableList.of("");
} }
this.value = this.values.get(0);
} }
/** @deprecated Use {@code TextInformationFrame(String id, String description, String[] values} instead */ /** @deprecated Use {@code TextInformationFrame(String id, String description, String[] values} instead */
@ -61,8 +60,8 @@ public final class TextInformationFrame extends Id3Frame {
/* package */ TextInformationFrame(Parcel in) { /* package */ TextInformationFrame(Parcel in) {
super(castNonNull(in.readString())); super(castNonNull(in.readString()));
description = in.readString(); description = in.readString();
values = in.createStringArray(); values = ImmutableList.copyOf(in.createStringArray());
this.value = values[0]; value = values.get(0);
} }
@Override @Override
@ -73,23 +72,23 @@ public final class TextInformationFrame extends Id3Frame {
switch (id) { switch (id) {
case "TT2": case "TT2":
case "TIT2": case "TIT2":
builder.setTitle(values[0]); builder.setTitle(values.get(0));
break; break;
case "TP1": case "TP1":
case "TPE1": case "TPE1":
builder.setArtist(String.join(MULTI_VALUE_DELIMITER, values)); builder.setArtist(String.join(", ", values));
break; break;
case "TP2": case "TP2":
case "TPE2": case "TPE2":
builder.setAlbumArtist(String.join(MULTI_VALUE_DELIMITER, values)); builder.setAlbumArtist(String.join(", ", values));
break; break;
case "TAL": case "TAL":
case "TALB": case "TALB":
builder.setAlbumTitle(values[0]); builder.setAlbumTitle(values.get(0));
break; break;
case "TRK": case "TRK":
case "TRCK": case "TRCK":
String[] trackNumbers = Util.split(values[0], "/"); String[] trackNumbers = Util.split(values.get(0), "/");
try { try {
int trackNumber = Integer.parseInt(trackNumbers[0]); int trackNumber = Integer.parseInt(trackNumbers[0]);
@Nullable @Nullable
@ -103,7 +102,7 @@ public final class TextInformationFrame extends Id3Frame {
case "TYE": case "TYE":
case "TYER": case "TYER":
try { try {
builder.setRecordingYear(Integer.parseInt(values[0])); builder.setRecordingYear(Integer.parseInt(values.get(0)));
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
// Do nothing, invalid input. // Do nothing, invalid input.
} }
@ -111,7 +110,7 @@ public final class TextInformationFrame extends Id3Frame {
case "TDA": case "TDA":
case "TDAT": case "TDAT":
try { try {
String date = values[0]; String date = values.get(0);
int month = Integer.parseInt(date.substring(2, 4)); int month = Integer.parseInt(date.substring(2, 4));
int day = Integer.parseInt(date.substring(0, 2)); int day = Integer.parseInt(date.substring(0, 2));
builder.setRecordingMonth(month).setRecordingDay(day); builder.setRecordingMonth(month).setRecordingDay(day);
@ -120,7 +119,7 @@ public final class TextInformationFrame extends Id3Frame {
} }
break; break;
case "TDRC": case "TDRC":
List<Integer> recordingDate = parseId3v2point4TimestampFrameForDate(values[0]); List<Integer> recordingDate = parseId3v2point4TimestampFrameForDate(values.get(0));
switch (recordingDate.size()) { switch (recordingDate.size()) {
case 3: case 3:
builder.setRecordingDay(recordingDate.get(2)); builder.setRecordingDay(recordingDate.get(2));
@ -138,7 +137,7 @@ public final class TextInformationFrame extends Id3Frame {
} }
break; break;
case "TDRL": case "TDRL":
List<Integer> releaseDate = parseId3v2point4TimestampFrameForDate(values[0]); List<Integer> releaseDate = parseId3v2point4TimestampFrameForDate(values.get(0));
switch (releaseDate.size()) { switch (releaseDate.size()) {
case 3: case 3:
builder.setReleaseDay(releaseDate.get(2)); builder.setReleaseDay(releaseDate.get(2));
@ -157,15 +156,15 @@ public final class TextInformationFrame extends Id3Frame {
break; break;
case "TCM": case "TCM":
case "TCOM": case "TCOM":
builder.setComposer(String.join(MULTI_VALUE_DELIMITER, values)); builder.setComposer(String.join(", ", values));
break; break;
case "TP3": case "TP3":
case "TPE3": case "TPE3":
builder.setConductor(String.join(MULTI_VALUE_DELIMITER, values)); builder.setConductor(String.join(", ", values));
break; break;
case "TXT": case "TXT":
case "TEXT": case "TEXT":
builder.setWriter(String.join(MULTI_VALUE_DELIMITER, values)); builder.setWriter(String.join(", ", values));
break; break;
default: default:
break; break;
@ -183,7 +182,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)
&& Arrays.equals(values, other.values); && values.equals(other.values);
} }
@Override @Override
@ -191,13 +190,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 + Arrays.hashCode(values); result = 31 * result + values.hashCode();
return result; return result;
} }
@Override @Override
public String toString() { public String toString() {
return id + ": description=" + description + ": value=" + String.join(MULTI_VALUE_DELIMITER, values); return id + ": description=" + description + ": value=" + String.join(", ", values);
} }
// Parcelable implementation. // Parcelable implementation.
@ -206,7 +205,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.writeStringArray(values); dest.writeStringArray(values.toArray(new String[0]));
} }
public static final Parcelable.Creator<TextInformationFrame> CREATOR = public static final Parcelable.Creator<TextInformationFrame> CREATOR =

View File

@ -52,7 +52,7 @@ public final class Id3DecoderTest {
TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
assertThat(textInformationFrame.id).isEqualTo("TXXX"); assertThat(textInformationFrame.id).isEqualTo("TXXX");
assertThat(textInformationFrame.description).isEmpty(); assertThat(textInformationFrame.description).isEmpty();
assertThat(textInformationFrame.values[0]).isEqualTo("mdialog_VINDICO1527664_start"); assertThat(textInformationFrame.values.get(0)).isEqualTo("mdialog_VINDICO1527664_start");
// Test UTF-16. // Test UTF-16.
rawId3 = rawId3 =
@ -67,7 +67,8 @@ public final class Id3DecoderTest {
textInformationFrame = (TextInformationFrame) metadata.get(0); textInformationFrame = (TextInformationFrame) metadata.get(0);
assertThat(textInformationFrame.id).isEqualTo("TXXX"); assertThat(textInformationFrame.id).isEqualTo("TXXX");
assertThat(textInformationFrame.description).isEqualTo("Hello World"); assertThat(textInformationFrame.description).isEqualTo("Hello World");
assertThat(textInformationFrame.values).isEmpty(); assertThat(textInformationFrame.values.size()).isEqualTo(1);
assertThat(textInformationFrame.values.get(0)).isEqualTo("");
// Test multiple values. // Test multiple values.
rawId3 = buildSingleFrameTag( rawId3 = buildSingleFrameTag(
@ -80,9 +81,9 @@ public final class Id3DecoderTest {
assertThat(metadata.length()).isEqualTo(1); assertThat(metadata.length()).isEqualTo(1);
textInformationFrame = (TextInformationFrame) metadata.get(0); textInformationFrame = (TextInformationFrame) metadata.get(0);
assertThat(textInformationFrame.description).isEqualTo("Hello World"); assertThat(textInformationFrame.description).isEqualTo("Hello World");
assertThat(textInformationFrame.values.length).isEqualTo(2); assertThat(textInformationFrame.values.size()).isEqualTo(2);
assertThat(textInformationFrame.values[0]).isEqualTo("Foo"); assertThat(textInformationFrame.values.get(0)).isEqualTo("Foo");
assertThat(textInformationFrame.values[1]).isEqualTo("Bar"); assertThat(textInformationFrame.values.get(1)).isEqualTo("Bar");
// Test empty. // Test empty.
rawId3 = buildSingleFrameTag("TXXX", new byte[0]); rawId3 = buildSingleFrameTag("TXXX", new byte[0]);
@ -96,7 +97,8 @@ public final class Id3DecoderTest {
textInformationFrame = (TextInformationFrame) metadata.get(0); textInformationFrame = (TextInformationFrame) metadata.get(0);
assertThat(textInformationFrame.id).isEqualTo("TXXX"); assertThat(textInformationFrame.id).isEqualTo("TXXX");
assertThat(textInformationFrame.description).isEmpty(); assertThat(textInformationFrame.description).isEmpty();
assertThat(textInformationFrame.values).isEmpty(); assertThat(textInformationFrame.values.size()).isEqualTo(1);
assertThat(textInformationFrame.values.get(0)).isEqualTo("");
} }
@Test @Test
@ -110,17 +112,17 @@ public final class Id3DecoderTest {
TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
assertThat(textInformationFrame.id).isEqualTo("TIT2"); assertThat(textInformationFrame.id).isEqualTo("TIT2");
assertThat(textInformationFrame.description).isNull(); assertThat(textInformationFrame.description).isNull();
assertThat(textInformationFrame.values.length).isEqualTo(1); assertThat(textInformationFrame.values.size()).isEqualTo(1);
assertThat(textInformationFrame.values[0]).isEqualTo("Hello World"); assertThat(textInformationFrame.values.get(0)).isEqualTo("Hello World");
// Test multiple values. // Test multiple values.
rawId3 = buildSingleFrameTag("TIT2", new byte[] {3, 70, 111, 111, 0, 66, 97, 114, 0}); rawId3 = buildSingleFrameTag("TIT2", new byte[] {3, 70, 111, 111, 0, 66, 97, 114, 0});
metadata = decoder.decode(rawId3, rawId3.length); metadata = decoder.decode(rawId3, rawId3.length);
assertThat(metadata.length()).isEqualTo(1); assertThat(metadata.length()).isEqualTo(1);
textInformationFrame = (TextInformationFrame) metadata.get(0); textInformationFrame = (TextInformationFrame) metadata.get(0);
assertThat(textInformationFrame.values.length).isEqualTo(2); assertThat(textInformationFrame.values.size()).isEqualTo(2);
assertThat(textInformationFrame.values[0]).isEqualTo("Foo"); assertThat(textInformationFrame.values.get(0)).isEqualTo("Foo");
assertThat(textInformationFrame.values[1]).isEqualTo("Bar"); assertThat(textInformationFrame.values.get(1)).isEqualTo("Bar");
// Test empty. // Test empty.
rawId3 = buildSingleFrameTag("TIT2", new byte[0]); rawId3 = buildSingleFrameTag("TIT2", new byte[0]);
@ -134,7 +136,8 @@ public final class Id3DecoderTest {
textInformationFrame = (TextInformationFrame) metadata.get(0); textInformationFrame = (TextInformationFrame) metadata.get(0);
assertThat(textInformationFrame.id).isEqualTo("TIT2"); assertThat(textInformationFrame.id).isEqualTo("TIT2");
assertThat(textInformationFrame.description).isNull(); assertThat(textInformationFrame.description).isNull();
assertThat(textInformationFrame.values).isEmpty(); assertThat(textInformationFrame.values.size()).isEqualTo(1);
assertThat(textInformationFrame.values.get(0)).isEqualTo("");
} }
@Test @Test

View File

@ -62,28 +62,28 @@ public class TextInformationFrameTest {
List<Metadata.Entry> entries = List<Metadata.Entry> entries =
ImmutableList.of( ImmutableList.of(
new TextInformationFrame(/* id= */ "TT2", /* description= */ null, /* value= */ title), new TextInformationFrame(/* id= */ "TT2", /* description= */ null, /* value= */ new String[] { title }),
new TextInformationFrame(/* id= */ "TP1", /* description= */ null, /* value= */ artist), new TextInformationFrame(/* id= */ "TP1", /* description= */ null, /* value= */ new String[] { artist }),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TAL", /* description= */ null, /* value= */ albumTitle), /* id= */ "TAL", /* description= */ null, /* value= */ new String[] { albumTitle }),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TP2", /* description= */ null, /* value= */ albumArtist), /* id= */ "TP2", /* description= */ null, /* value= */ new String[] { albumArtist }),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TRK", /* description= */ null, /* value= */ trackNumberInfo), /* id= */ "TRK", /* description= */ null, /* value= */ new String[] { trackNumberInfo}),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TYE", /* description= */ null, /* value= */ recordingYear), /* id= */ "TYE", /* description= */ null, /* value= */ new String[] { recordingYear }),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TDA", /* id= */ "TDA",
/* description= */ null, /* description= */ null,
/* value= */ recordingDay + recordingMonth), /* value= */ new String[] { recordingDay + recordingMonth }),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TDRL", /* description= */ null, /* value= */ releaseDate), /* id= */ "TDRL", /* description= */ null, /* value= */ new String[] { releaseDate }),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TCM", /* description= */ null, /* value= */ composer), /* id= */ "TCM", /* description= */ null, /* value= */ new String[] { composer }),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TP3", /* description= */ null, /* value= */ conductor), /* id= */ "TP3", /* description= */ null, /* value= */ new String[] { conductor }),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TXT", /* description= */ null, /* value= */ writer)); /* id= */ "TXT", /* description= */ null, /* value= */ new String[] { writer }));
MediaMetadata.Builder builder = MediaMetadata.EMPTY.buildUpon(); MediaMetadata.Builder builder = MediaMetadata.EMPTY.buildUpon();
for (Metadata.Entry entry : entries) { for (Metadata.Entry entry : entries) {
@ -107,5 +107,21 @@ public class TextInformationFrameTest {
assertThat(mediaMetadata.composer.toString()).isEqualTo(composer); assertThat(mediaMetadata.composer.toString()).isEqualTo(composer);
assertThat(mediaMetadata.conductor.toString()).isEqualTo(conductor); assertThat(mediaMetadata.conductor.toString()).isEqualTo(conductor);
assertThat(mediaMetadata.writer.toString()).isEqualTo(writer); assertThat(mediaMetadata.writer.toString()).isEqualTo(writer);
// Test empty value array
entries =
ImmutableList.of(
new TextInformationFrame(/* id= */ "TT2", /* description= */ null, /* value= */ new String[0])
);
builder = MediaMetadata.EMPTY.buildUpon();
for (Metadata.Entry entry : entries) {
entry.populateMediaMetadata(builder);
}
mediaMetadata = builder.build();
assertThat(mediaMetadata.title.toString()).isEqualTo("");
} }
} }