mirror of
https://github.com/samsonjs/media.git
synced 2026-04-18 13:25:47 +00:00
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.
This commit is contained in:
parent
ab4d37f499
commit
b125a2b0b3
2 changed files with 75 additions and 25 deletions
|
|
@ -29,8 +29,10 @@ import java.io.UnsupportedEncodingException;
|
|||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.w3c.dom.Text;
|
||||
|
||||
/** Decodes ID3 tags. */
|
||||
public final class Id3Decoder extends SimpleMetadataDecoder {
|
||||
|
|
@ -458,11 +460,23 @@ public final class Id3Decoder extends SimpleMetadataDecoder {
|
|||
int descriptionEndIndex = indexOfEos(data, 0, encoding);
|
||||
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 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);
|
||||
|
||||
return new TextInformationFrame("TXXX", description, value);
|
||||
valueStartIndex = valueEndIndex + delimiterLength(encoding);
|
||||
valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
|
||||
}
|
||||
|
||||
|
||||
return new TextInformationFrame("TXXX", description, values.toArray(new String[0]));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
@ -479,10 +493,22 @@ public final class Id3Decoder extends SimpleMetadataDecoder {
|
|||
byte[] data = new byte[frameSize - 1];
|
||||
id3Data.readBytes(data, 0, frameSize - 1);
|
||||
|
||||
int valueEndIndex = indexOfEos(data, 0, encoding);
|
||||
String value = new String(data, 0, valueEndIndex, 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<>();
|
||||
|
||||
return new TextInformationFrame(id, null, value);
|
||||
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, values.toArray(new String[0]));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
|
|||
|
|
@ -23,48 +23,71 @@ import androidx.annotation.Nullable;
|
|||
import com.google.android.exoplayer2.MediaMetadata;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/** Text information ID3 frame. */
|
||||
public final class TextInformationFrame extends Id3Frame {
|
||||
private final static String MULTI_VALUE_DELIMITER = ", ";
|
||||
|
||||
@Nullable public final String description;
|
||||
|
||||
/** @deprecated Use {@code values} instead. */
|
||||
@Deprecated
|
||||
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);
|
||||
if (values.length == 0) {
|
||||
throw new IllegalArgumentException("A text information frame must have at least one value.");
|
||||
}
|
||||
|
||||
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) {
|
||||
super(castNonNull(in.readString()));
|
||||
description = in.readString();
|
||||
value = castNonNull(in.readString());
|
||||
values = new String[] {};
|
||||
in.readStringArray(values);
|
||||
this.value = values[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
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) {
|
||||
case "TT2":
|
||||
case "TIT2":
|
||||
builder.setTitle(value);
|
||||
builder.setTitle(values[0]);
|
||||
break;
|
||||
case "TP1":
|
||||
case "TPE1":
|
||||
builder.setArtist(value);
|
||||
builder.setArtist(String.join(MULTI_VALUE_DELIMITER, values));
|
||||
break;
|
||||
case "TP2":
|
||||
case "TPE2":
|
||||
builder.setAlbumArtist(value);
|
||||
builder.setAlbumArtist(String.join(MULTI_VALUE_DELIMITER, values));
|
||||
break;
|
||||
case "TAL":
|
||||
case "TALB":
|
||||
builder.setAlbumTitle(value);
|
||||
builder.setAlbumTitle(values[0]);
|
||||
break;
|
||||
case "TRK":
|
||||
case "TRCK":
|
||||
String[] trackNumbers = Util.split(value, "/");
|
||||
String[] trackNumbers = Util.split(values[0], "/");
|
||||
try {
|
||||
int trackNumber = Integer.parseInt(trackNumbers[0]);
|
||||
@Nullable
|
||||
|
|
@ -78,7 +101,7 @@ public final class TextInformationFrame extends Id3Frame {
|
|||
case "TYE":
|
||||
case "TYER":
|
||||
try {
|
||||
builder.setRecordingYear(Integer.parseInt(value));
|
||||
builder.setRecordingYear(Integer.parseInt(values[0]));
|
||||
} catch (NumberFormatException e) {
|
||||
// Do nothing, invalid input.
|
||||
}
|
||||
|
|
@ -86,15 +109,16 @@ public final class TextInformationFrame extends Id3Frame {
|
|||
case "TDA":
|
||||
case "TDAT":
|
||||
try {
|
||||
int month = Integer.parseInt(value.substring(2, 4));
|
||||
int day = Integer.parseInt(value.substring(0, 2));
|
||||
String date = values[0];
|
||||
int month = Integer.parseInt(date.substring(2, 4));
|
||||
int day = Integer.parseInt(date.substring(0, 2));
|
||||
builder.setRecordingMonth(month).setRecordingDay(day);
|
||||
} catch (NumberFormatException | StringIndexOutOfBoundsException e) {
|
||||
// Do nothing, invalid input.
|
||||
}
|
||||
break;
|
||||
case "TDRC":
|
||||
List<Integer> recordingDate = parseId3v2point4TimestampFrameForDate(value);
|
||||
List<Integer> recordingDate = parseId3v2point4TimestampFrameForDate(values[0]);
|
||||
switch (recordingDate.size()) {
|
||||
case 3:
|
||||
builder.setRecordingDay(recordingDate.get(2));
|
||||
|
|
@ -112,7 +136,7 @@ public final class TextInformationFrame extends Id3Frame {
|
|||
}
|
||||
break;
|
||||
case "TDRL":
|
||||
List<Integer> releaseDate = parseId3v2point4TimestampFrameForDate(value);
|
||||
List<Integer> releaseDate = parseId3v2point4TimestampFrameForDate(values[0]);
|
||||
switch (releaseDate.size()) {
|
||||
case 3:
|
||||
builder.setReleaseDay(releaseDate.get(2));
|
||||
|
|
@ -131,15 +155,15 @@ public final class TextInformationFrame extends Id3Frame {
|
|||
break;
|
||||
case "TCM":
|
||||
case "TCOM":
|
||||
builder.setComposer(value);
|
||||
builder.setComposer(String.join(MULTI_VALUE_DELIMITER, values));
|
||||
break;
|
||||
case "TP3":
|
||||
case "TPE3":
|
||||
builder.setConductor(value);
|
||||
builder.setConductor(String.join(MULTI_VALUE_DELIMITER, values));
|
||||
break;
|
||||
case "TXT":
|
||||
case "TEXT":
|
||||
builder.setWriter(value);
|
||||
builder.setWriter(String.join(MULTI_VALUE_DELIMITER, values));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
@ -157,7 +181,7 @@ public final class TextInformationFrame extends Id3Frame {
|
|||
TextInformationFrame other = (TextInformationFrame) obj;
|
||||
return Util.areEqual(id, other.id)
|
||||
&& Util.areEqual(description, other.description)
|
||||
&& Util.areEqual(value, other.value);
|
||||
&& Util.areEqual(values, other.values);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -165,13 +189,13 @@ public final class TextInformationFrame extends Id3Frame {
|
|||
int result = 17;
|
||||
result = 31 * result + id.hashCode();
|
||||
result = 31 * result + (description != null ? description.hashCode() : 0);
|
||||
result = 31 * result + (value != null ? value.hashCode() : 0);
|
||||
result = 31 * result + Arrays.hashCode(values);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return id + ": description=" + description + ": value=" + value;
|
||||
return id + ": description=" + description + ": values=" + String.join(MULTI_VALUE_DELIMITER, values);
|
||||
}
|
||||
|
||||
// Parcelable implementation.
|
||||
|
|
@ -180,7 +204,7 @@ public final class TextInformationFrame extends Id3Frame {
|
|||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(id);
|
||||
dest.writeString(description);
|
||||
dest.writeString(value);
|
||||
dest.writeStringArray(values);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<TextInformationFrame> CREATOR =
|
||||
|
|
|
|||
Loading…
Reference in a new issue