diff --git a/.gitignore b/.gitignore index e71582c6..3e5c8484 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ -.DS_Store -bin -test-bin -build -*.iml -.idea -*.project -*.classpath -target -.settings +.DS_Store +bin +test-bin +build +*.iml +.idea +*.project +*.classpath +target +.settings +/nbproject/ \ No newline at end of file diff --git a/src/main/java/com/mpatric/mp3agic/AbstractID3v2Tag.java b/src/main/java/com/mpatric/mp3agic/AbstractID3v2Tag.java index ec3c06b7..8893fc4d 100644 --- a/src/main/java/com/mpatric/mp3agic/AbstractID3v2Tag.java +++ b/src/main/java/com/mpatric/mp3agic/AbstractID3v2Tag.java @@ -1096,6 +1096,30 @@ public void setWmpRating(final int rating) { } } + @Override + public String getCustomText(String description) { + ID3v2TXXXFrameData frameData = ID3v2TXXXFrameData.extract( + frameSets, + unsynchronisation, + description); + + if (frameData != null) { + return frameData.getValue().toString(); + } + + return null; + } + + @Override + public void setCustomText(String description, String value) { + ID3v2TXXXFrameData.createOrAddField( + frameSets, + unsynchronisation, + description, + value, + true); + } + private ArrayList extractChapterFrameData(String id) { ID3v2FrameSet frameSet = frameSets.get(id); if (frameSet != null) { diff --git a/src/main/java/com/mpatric/mp3agic/ID3v2.java b/src/main/java/com/mpatric/mp3agic/ID3v2.java index 703dda36..42de4d42 100644 --- a/src/main/java/com/mpatric/mp3agic/ID3v2.java +++ b/src/main/java/com/mpatric/mp3agic/ID3v2.java @@ -131,6 +131,10 @@ public interface ID3v2 extends ID3v1 { void setLyrics(String lyrics); + String getCustomText(String description); + + void setCustomText(String description, String value); + /** * Set genre from text. * This method behaves different depending on the ID3 version. diff --git a/src/main/java/com/mpatric/mp3agic/ID3v2TXXXFrameData.java b/src/main/java/com/mpatric/mp3agic/ID3v2TXXXFrameData.java new file mode 100644 index 00000000..ae998fd5 --- /dev/null +++ b/src/main/java/com/mpatric/mp3agic/ID3v2TXXXFrameData.java @@ -0,0 +1,245 @@ +package com.mpatric.mp3agic; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class ID3v2TXXXFrameData extends AbstractID3v2FrameData { + + protected static final String TAG = "ID3::TXXX"; + + public static final String ID_FIELD = "TXXX"; + + private EncodedText description; + private EncodedText value; + + public ID3v2TXXXFrameData(boolean unsynchronisation) { + super(unsynchronisation); + } + + public ID3v2TXXXFrameData(boolean unsynchronisation, EncodedText description, EncodedText value) { + super(unsynchronisation); + this.description = description; + this.value = value; + } + + public ID3v2TXXXFrameData(boolean unsynchronisation, byte[] bytes) throws InvalidDataException { + super(unsynchronisation); + synchroniseAndUnpackFrameData(bytes); + } + + @Override + protected void unpackFrameData(byte[] bytes) throws InvalidDataException { + int marker = BufferTools.indexOfTerminatorForEncoding(bytes, 1, bytes[0]); + if (marker >= 0) { + description = new EncodedText(bytes[0], BufferTools.copyBuffer(bytes, 1, marker - 1)); + marker += description.getTerminator().length; + } else { + description = new EncodedText(bytes[0], ""); + marker = 1; + } + + if (bytes.length - marker >= 0) { + value = new EncodedText(bytes[0], BufferTools.copyBuffer(bytes, marker, bytes.length - marker)); + } else { + value = new EncodedText(bytes[0], ""); + } + } + + @Override + protected byte[] packFrameData() { + byte[] bytes = new byte[getLength()]; + if (value != null) { + bytes[0] = value.getTextEncoding(); + } else { + bytes[0] = 0; + } + int marker = 1; + if (description != null) { + byte[] descriptionBytes = description.toBytes(true, true); + BufferTools.copyIntoByteBuffer(descriptionBytes, 0, descriptionBytes.length, bytes, marker); + marker += descriptionBytes.length; + } else { + bytes[marker++] = 0; + } + if (value != null) { + byte[] commentBytes = value.toBytes(true, false); + BufferTools.copyIntoByteBuffer(commentBytes, 0, commentBytes.length, bytes, marker); + } + return bytes; + } + + @Override + protected int getLength() { + int length = 1; + if (description != null) { + length += description.toBytes(true, true).length; + } else { + length++; + } + if (value != null) { + length += value.toBytes(true, false).length; + } + return length; + } + + public EncodedText getValue() { + return value; + } + + public void setValue(EncodedText value) { + this.value = value; + } + + public EncodedText getDescription() { + return description; + } + + public void setDescription(EncodedText description) { + this.description = description; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + result = prime * result + ((description == null) ? 0 : description.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ID3v2TXXXFrameData other = (ID3v2TXXXFrameData) obj; + if (!Objects.equals(this.description, other.description)) { + return false; + } + if (!Objects.equals(this.value, other.value)) { + return false; + } + return true; + } + + public static Map extractAllFrames( + Map frameSets, + boolean useFrameUnsynchronisation, + String description) { + ID3v2FrameSet frameSet = frameSets.get(ID_FIELD); + + HashMap frames = new HashMap<>(); + + if (frameSet == null) { + return frames; + } + + for (ID3v2Frame frame : frameSet.getFrames()) { + try { + ID3v2TXXXFrameData field = new ID3v2TXXXFrameData( + useFrameUnsynchronisation, + frame.getData()); + + if (description == null || field.getDescription().toString().contains(description)) { + frames.put(frame, field); + } + } catch (InvalidDataException e) { + e.printStackTrace(); + } + } + + return frames; + } + + public static ArrayList extractAll( + Map frameSets, + boolean useFrameUnsynchronisation, + String description) { + + Map frames = extractAllFrames( + frameSets, + useFrameUnsynchronisation, + description); + + ArrayList fields = new ArrayList<>(); + + for (Map.Entry item : frames.entrySet()) { + fields.add(item.getValue()); + } + + return fields; + } + + public static ArrayList extractAll( + Map frameSets, + boolean useFrameUnsynchronisation) { + + return extractAll(frameSets, useFrameUnsynchronisation, null); + } + + public static ID3v2TXXXFrameData extract( + Map frameSets, + boolean useFrameUnsynchronisation, + String description) { + + ArrayList items = extractAll( + frameSets, + useFrameUnsynchronisation, + description); + + if (items.size() > 0) { + return items.get(0); + } + + return null; + } + + public static void createOrAddField( + Map frameSets, + boolean useFrameUnsynchronisation, + String description, + String value, + boolean useDescriptionToMatch + ) { + + ID3v2TXXXFrameData field = new ID3v2TXXXFrameData( + useFrameUnsynchronisation, + new EncodedText(description), + new EncodedText(value)); + + ID3v2Frame frame = new ID3v2Frame( + ID_FIELD, + field.toBytes()); + + ID3v2FrameSet frameSet = frameSets.get(frame.getId()); + if (frameSet == null) { + frameSet = new ID3v2FrameSet(frame.getId()); + frameSet.addFrame(frame); + frameSets.put(frame.getId(), frameSet); + } else { + if (useDescriptionToMatch) { + Map frames = extractAllFrames( + frameSets, + useFrameUnsynchronisation, + description); + + if (frames != null && !frames.isEmpty()) { + frameSet.getFrames().removeAll(frames.keySet()); + } + } + + frameSet.addFrame(frame); + } + + } + +} diff --git a/src/test/java/com/mpatric/mp3agic/ID3WrapperTest.java b/src/test/java/com/mpatric/mp3agic/ID3WrapperTest.java index c08a72f3..70ce8b92 100644 --- a/src/test/java/com/mpatric/mp3agic/ID3WrapperTest.java +++ b/src/test/java/com/mpatric/mp3agic/ID3WrapperTest.java @@ -1402,6 +1402,15 @@ public void setLyrics(String lyrics) { this.lyrics = lyrics; } + @Override + public String getCustomText(String description) { + return null; + } + + @Override + public void setCustomText(String description, String value) { + } + @Override public void setGenreDescription(String text) { this.genreDescription = text; diff --git a/src/test/java/com/mpatric/mp3agic/ID3v2TXXXFrameDataTest.java b/src/test/java/com/mpatric/mp3agic/ID3v2TXXXFrameDataTest.java new file mode 100644 index 00000000..0291e113 --- /dev/null +++ b/src/test/java/com/mpatric/mp3agic/ID3v2TXXXFrameDataTest.java @@ -0,0 +1,104 @@ +package com.mpatric.mp3agic; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +public class ID3v2TXXXFrameDataTest { + + @Test + public void test_input() throws Exception { + Map frameSets = new HashMap<>(); + + // for input of new field + ID3v2TXXXFrameData.createOrAddField( + frameSets, + true, + "my_custom_text", + "value", + true); + + assertEquals(1, frameSets.size()); + } + + @Test + public void test_extraction() throws Exception { + Map frameSets = new HashMap<>(); + + ID3v2TXXXFrameData.createOrAddField( + frameSets, + true, + "my_custom_text", + "value", + true); + + // for extraction + ID3v2TXXXFrameData frameData = ID3v2TXXXFrameData.extract( + frameSets, + true, + "my_custom_text"); + + assertEquals("my_custom_text", frameData.getDescription().toString()); + assertEquals("value", frameData.getValue().toString()); + } + + @Test + public void test_input_replacement() throws Exception { + Map frameSets = new HashMap<>(); + + ID3v2TXXXFrameData.createOrAddField( + frameSets, + true, + "my_custom_text", + "value", + true); + assertEquals(1, frameSets.size()); + + // for input with replacement + ID3v2TXXXFrameData.createOrAddField( + frameSets, + true, + "my_custom_text", + "value changed", + true); + ID3v2TXXXFrameData frameData = ID3v2TXXXFrameData.extract( + frameSets, + true, + "my_custom_text"); + + assertEquals("my_custom_text", frameData.getDescription().toString()); + assertEquals("value changed", frameData.getValue().toString()); + } + + @Test + public void test_input_no_replacement() throws Exception { + Map frameSets = new HashMap<>(); + + ID3v2TXXXFrameData.createOrAddField( + frameSets, + true, + "my_custom_text", + "value", + true); + assertEquals(1, frameSets.size()); + + // for input with-out replacement + ID3v2TXXXFrameData.createOrAddField( + frameSets, + true, + "my_custom_text", + "value 2", + false); + ArrayList frameDatas = ID3v2TXXXFrameData.extractAll( + frameSets, + true, + "my_custom_text"); + + assertEquals(2, frameDatas.size()); + } + +} diff --git a/src/test/java/com/mpatric/mp3agic/ID3v2TagTest.java b/src/test/java/com/mpatric/mp3agic/ID3v2TagTest.java index 99e89348..98596c6b 100644 --- a/src/test/java/com/mpatric/mp3agic/ID3v2TagTest.java +++ b/src/test/java/com/mpatric/mp3agic/ID3v2TagTest.java @@ -516,8 +516,17 @@ public void shouldIgnoreInvalidWmpRatingOnWrite() throws Exception { final int invalidValue = 6; id3tag.setWmpRating(invalidValue); assertEquals(originalValue, id3tag.getWmpRating()); - } + } + + @Test + public void shouldGetSetCustomText() throws Exception { + Mp3File mp3File = new Mp3File("src/test/resources/v23tagwithchapters.mp3"); + ID3v2 id3tag = mp3File.getId3v2Tag(); + assertNull(id3tag.getCustomText("foo")); + id3tag.setCustomText("foo", "bar"); + assertEquals("bar", id3tag.getCustomText("foo")); + } private void setTagFields(ID3v2 id3tag) throws IOException { id3tag.setTrack("1");