package org.farng.mp3.lyrics3; import java.io.IOException; import java.io.RandomAccessFile; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.farng.mp3.AbstractMP3Tag; import org.farng.mp3.InvalidTagException; import org.farng.mp3.TagException; import org.farng.mp3.TagNotFoundException; import org.farng.mp3.TagOptionSingleton; import org.farng.mp3.id3.AbstractID3v2Frame; import org.farng.mp3.id3.ID3v1; import org.farng.mp3.id3.ID3v2_4; /** * <TABLE border=0> <TBODY> <TR> <TD class=h2>What is Lyrics3 v2.00?</TD></TR></TBODY></TABLE> <TABLE border=0> <TBODY> * <TR vAlign=top> <TD> <P>The Lyrics3 v2.00 tag is more complicated than the previous Lyrics3 tag but has a lot more * capabilities. Just like the old Lyrics3 tag it resides between the audio and the ID3 tag, which must be present. The * tag uses only text for everything from content to size descriptors, which are represented as numerical strings. This * makes it easier to implement a Lyrics3 v2.00 reader. At least if BASIC is your language of choice.</P> * <p/> * <P>The Lyrics3 block, after the MP3 audio and before the ID3 tag, begins with the word "LYRICSBEGIN" after which a * number of field records follows. The Lyrics3 block ends with a six character size descriptor and the string * "LYRICS200". The size value includes the "LYRICSBEGIN" and "LYRICS200" strings.</P> * <p/> * <P>Lyrics2 v2.00 uses somthing called fields to represent information. The data in a field can consist of ASCII * characters in the range 01 to 254 according to the standard. As the ASCII character map is only defined from 00 to * 128 ISO-8859-1 might be assumed. Numerical fields are 5 or 6 characters long, depending on location, and are padded * with zeroes. * <p/> * <P>Only the size of the tag sets the limit for how many fields may be present. All fields uses a simple structure * that includes a three character field ID, six characters describing the size of the information and the actual * information. This makes it possible to read unknown fields and write them back when saving the tag. There are no * required fields in the tag, but at least one field must exist. Fields can appear in any order in the tag, except the * indication field which must be the first field if used. Fields that include more then one line uses [CR][LF] * delimiters between lines.</P></TD> </TR></TBODY></TABLE> <BR>  <TABLE border=0> <TBODY> <TR> <TD * class=h2>Defined fields</TD></TR></TBODY></TABLE> <TABLE border=0> <TBODY> <TR vAlign=top> <TD> <P>The following list * is a list of currently defined field IDs. More fields might be added if needed on newer versions of the Lyrics3 v2.00 * specifications. Unknown fields should be ignored.</P> <TABLE> <TBODY> <TR> <TD><U>ID</U></TD> <TD><U>Max * size</U></TD> <TD><U>Description</U></TD></TR> <TR vAlign=top> <TD><B>IND</B></TD> <TD>00002</TD> <TD>Indications * field. This is always two characters big in v2.00, but might be bigger in a future standard. The first byte indicates * wether or not a lyrics field is present. "1" for present and "0" for otherwise. The second character indicates if * there is a timestamp in the lyrics. Again "1" for yes and "0" for no.</TD></TR> <TR vAlign=top> <TD><B>LYR</B></TD> * <TD>99999</TD> <TD>Lyrics multi line text. Timestamps can be used anywhere in the text in any order. Timestamp format * is [mm:ss] (no spaces allowed in the timestamps).</TD></TR> <TR vAlign=top> <TD><B>INF</B></TD> <TD>99999</TD> * <TD>Additional information multi line text.</TD></TR> <TR vAlign=top> <TD><B>AUT</B></TD> <TD>00250</TD> * <TD>Lyrics/Music Author name.</TD></TR> <TR vAlign=top> <TD><B>EAL</B></TD> <TD>00250</TD> <TD>Extended Album * name.</TD></TR> <TR vAlign=top> <TD><B>EAR</B></TD> <TD>00250</TD> <TD>Extended Artist name.</TD></TR> <TR * vAlign=top> <TD><B>ETT</B></TD> <TD>00250</TD> <TD>Extended Track Title.</TD></TR> <TR vAlign=top> * <TD><B>IMG</B></TD> <TD>99999</TD> <TD>Link to an image files (BMP or JPG format). Image lines include filename, * description and timestamp separated by delimiter - two ASCII chars 124 ("||"). Description and timestamp are * optional, but if timestamp is used, and there is no description, two delimiters ("||||") should be used between the * filename and the timestamp. Multiple images are allowed by using a [CR][LF] delimiter between each image line. No * [CR][LF] is needed after the last image line. Number of images is not limited (except by the field * size).<BR><B>Filename</B> can be in one of these formats: <UL> <LI>Filename only - when the image is located in the * same path as the MP3 file (preferred, since if you move the mp3 file this will still be correct) <LI>Relative Path + * Filename - when the image is located in a subdirectory below the MP3 file (i.e. images\cover.jpg) <LI>Full path + * Filename - when the image is located in a totally different path or drive. This will not work if the image is moved * or drive letters has changed, and so should be avoided if possible (i.e. c:\images\artist.jpg)</LI></UL><B>Description</B> * can be up to 250 chars long.<BR><B>Timestamp</B> must be formatted like the lyrics timestamp which is "[mm:ss]". If * an image has a timestamp, then the visible image will automatically switch to that image on the timestamp play time, * just the same as the selected lyrics line is switched based on timestamps.</TD></TR></TBODY></TABLE> * </TD></TR></TBODY></TABLE>   <TABLE border=0> <TBODY> <TR vAlign=top> <TD> <P>The extended Album, Artist and * Track are an extension to the fields in the ID3v1 tag - which are limited to 30 chars. If these extended fields * exist, make sure their first 30 chars are exactly the same as the ones in the ID3v1 tag. If they are the same, * display the extended field. If not, display the one from the ID tag. These 'mismatched' extended fields, should be * removed when saving the lyrics tag.</P> * <p/> * <P>When saving the extended fields, make sure to copy the first 30 chars of each field to the ID3 tag matching * fields. It is recommended NOT to save extended fields at all, if they are not larger then 30 * chars.</P></TD></TR></TBODY></TABLE> * * @author Eric Farng * @version $Revision: 2374 $ */ public class Lyrics3v2 extends AbstractLyrics3 { private Map fieldMap = new HashMap(8); /** * Creates a new Lyrics3v2 object. */ public Lyrics3v2() { super(); } /** * Creates a new Lyrics3v2 object. */ public Lyrics3v2(final Lyrics3v2 copyObject) { super(copyObject); final Iterator iterator = copyObject.fieldMap.keySet().iterator(); String oldIdentifier; String newIdentifier; Lyrics3v2Field newObject; while (iterator.hasNext()) { oldIdentifier = iterator.next().toString(); newIdentifier = oldIdentifier; newObject = new Lyrics3v2Field((Lyrics3v2Field) copyObject.fieldMap.get(newIdentifier)); fieldMap.put(newIdentifier, newObject); } } /** * Creates a new Lyrics3v2 object. */ public Lyrics3v2(final AbstractMP3Tag mp3tag) { super(); if (mp3tag != null) { // upgrade the tag to lyrics3v2 if (mp3tag instanceof Lyrics3v2) { throw new UnsupportedOperationException("Copy Constructor not called. Please type cast the argument"); } else if (mp3tag instanceof Lyrics3v1) { final Lyrics3v1 lyricOld = (Lyrics3v1) mp3tag; final Lyrics3v2Field newField = new Lyrics3v2Field(new FieldBodyLYR(lyricOld.getLyric())); fieldMap.put(newField.getIdentifier(), newField); } else { Lyrics3v2Field newField; final Iterator iterator; iterator = (new ID3v2_4(mp3tag)).iterator(); while (iterator.hasNext()) { try { newField = new Lyrics3v2Field((AbstractID3v2Frame) iterator.next()); this.fieldMap.put(newField.getIdentifier(), newField); } catch (TagException ex) { //invalid frame to create lyrics3 field. ignore and // keep going } } } } } /** * Creates a new Lyrics3v2 object. */ public Lyrics3v2(final RandomAccessFile file) throws TagNotFoundException, IOException { this.read(file); } public void setField(final Lyrics3v2Field field) { if (field.getBody() != null) { this.fieldMap.put(field.getIdentifier(), field); } } /** * Gets the value of the frame identified by identifier * * @param identifier The three letter code * * @return The value associated with the identifier */ public Lyrics3v2Field getField(final String identifier) { return (Lyrics3v2Field) this.fieldMap.get(identifier); } public int getFieldCount() { return this.fieldMap.size(); } public String getIdentifier() { return "Lyrics3v2.00"; } public int getSize() { int size = 0; final Iterator iterator = this.fieldMap.values().iterator(); Lyrics3v2Field field; while (iterator.hasNext()) { field = (Lyrics3v2Field) iterator.next(); size += field.getSize(); } // include LYRICSBEGIN, but not 6 char size or LYRICSEND return 11 + size; } public void append(final AbstractMP3Tag tag) { final Lyrics3v2 oldTag = this; final Lyrics3v2 newTag; if (tag != null) { if (tag instanceof Lyrics3v2) { newTag = (Lyrics3v2) tag; } else { newTag = new Lyrics3v2(tag); } Iterator iterator = newTag.fieldMap.values().iterator(); Lyrics3v2Field field; AbstractLyrics3v2FieldBody body; while (iterator.hasNext()) { field = (Lyrics3v2Field) iterator.next(); if (oldTag.hasField(field.getIdentifier()) == false) { oldTag.setField(field); } else { body = (AbstractLyrics3v2FieldBody) oldTag.getField(field.getIdentifier()).getBody(); final boolean save = TagOptionSingleton.getInstance().getLyrics3SaveField(field.getIdentifier()); if ((body.getSize() == 0) && save) { oldTag.setField(field); } } } // reset tag options to save all current fields. iterator = oldTag.fieldMap.keySet().iterator(); String id; while (iterator.hasNext()) { id = (String) iterator.next(); TagOptionSingleton.getInstance().setLyrics3SaveField(id, true); } } } public boolean equals(final Object obj) { if ((obj instanceof Lyrics3v2) == false) { return false; } final Lyrics3v2 lyrics3v2 = (Lyrics3v2) obj; if (this.fieldMap.equals(lyrics3v2.fieldMap) == false) { return false; } return super.equals(obj); } public boolean hasField(final String identifier) { return this.fieldMap.containsKey(identifier); } public Iterator iterator() { return this.fieldMap.values().iterator(); } public void overwrite(final AbstractMP3Tag tag) { final Lyrics3v2 oldTag = this; final Lyrics3v2 newTag; if (tag != null) { if (tag instanceof Lyrics3v2) { newTag = (Lyrics3v2) tag; } else { newTag = new Lyrics3v2(tag); } Iterator iterator = newTag.fieldMap.values().iterator(); Lyrics3v2Field field; while (iterator.hasNext()) { field = (Lyrics3v2Field) iterator.next(); if (TagOptionSingleton.getInstance().getLyrics3SaveField(field.getIdentifier())) { oldTag.setField(field); } } // reset tag options to save all current fields. iterator = oldTag.fieldMap.keySet().iterator(); String id; while (iterator.hasNext()) { id = (String) iterator.next(); TagOptionSingleton.getInstance().setLyrics3SaveField(id, true); } } } public void read(final RandomAccessFile file) throws TagNotFoundException, IOException { final long filePointer; final int lyricSize; if (seek(file)) { lyricSize = seekSize(file); } else { throw new TagNotFoundException("Lyrics3v2.00 Tag Not Found"); } // reset file pointer to the beginning of the tag; seek(file); filePointer = file.getFilePointer(); this.fieldMap = new HashMap(); Lyrics3v2Field lyric; // read each of the fields while ((file.getFilePointer() - filePointer) < (lyricSize - 11)) { try { lyric = new Lyrics3v2Field(file); setField(lyric); } catch (InvalidTagException ex) { // keep reading until we're done } } } public void removeField(final String identifier) { this.fieldMap.remove(identifier); } public boolean seek(final RandomAccessFile file) throws IOException { final byte[] buffer = new byte[11]; String lyricEnd; final String lyricStart; long filePointer; final long lyricSize; // check right before the ID3 1.0 tag for the lyrics tag file.seek(file.length() - 128 - 9); file.read(buffer, 0, 9); lyricEnd = new String(buffer, 0, 9); if (lyricEnd.equals("LYRICS200")) { filePointer = file.getFilePointer(); } else { // check the end of the file for a lyrics tag incase an ID3 // tag wasn't placed after it. file.seek(file.length() - 9); file.read(buffer, 0, 9); lyricEnd = new String(buffer, 0, 9); if (lyricEnd.equals("LYRICS200")) { filePointer = file.getFilePointer(); } else { return false; } } // read the 6 bytes for the length of the tag filePointer -= (9 + 6); file.seek(filePointer); file.read(buffer, 0, 6); lyricSize = Integer.parseInt(new String(buffer, 0, 6)); // read the lyrics begin tag if it exists. file.seek(filePointer - lyricSize); file.read(buffer, 0, 11); lyricStart = new String(buffer, 0, 11); return lyricStart.equals("LYRICSBEGIN") == true; } public String toString() { final Iterator iterator = this.fieldMap.values().iterator(); Lyrics3v2Field field; String str = getIdentifier() + " " + this.getSize() + "\n"; while (iterator.hasNext()) { field = (Lyrics3v2Field) iterator.next(); str += (field.toString() + "\n"); } return str; } public void updateField(final String identifier) { Lyrics3v2Field lyrField; if (identifier.equals("IND")) { final boolean lyricsPresent = this.fieldMap.containsKey("LYR"); boolean timeStampPresent = false; if (lyricsPresent) { lyrField = (Lyrics3v2Field) this.fieldMap.get("LYR"); final FieldBodyLYR lyrBody = (FieldBodyLYR) lyrField.getBody(); timeStampPresent = lyrBody.hasTimeStamp(); } lyrField = new Lyrics3v2Field(new FieldBodyIND(lyricsPresent, timeStampPresent)); setField(lyrField); } } public void write(final AbstractMP3Tag tag) { final Lyrics3v2 oldTag = this; final Lyrics3v2 newTag; if (tag != null) { if (tag instanceof Lyrics3v2) { newTag = (Lyrics3v2) tag; } else { newTag = new Lyrics3v2(tag); } final Iterator iterator = newTag.fieldMap.values().iterator(); Lyrics3v2Field field; oldTag.fieldMap.clear(); while (iterator.hasNext()) { field = (Lyrics3v2Field) iterator.next(); oldTag.setField(field); } } } public void write(final RandomAccessFile file) throws IOException { int offset = 0; final long filePointer; final byte[] buffer = new byte[6 + 9]; String str; Lyrics3v2Field field; final Iterator iterator; ID3v1 id3v1tag = new ID3v1(); id3v1tag = id3v1tag.getID3tag(file); delete(file); file.seek(file.length()); filePointer = file.getFilePointer(); str = "LYRICSBEGIN"; for (int i = 0; i < str.length(); i++) { buffer[i] = (byte) str.charAt(i); } file.write(buffer, 0, str.length()); // IND needs to go first. lets create/update it and write it first. updateField("IND"); field = (Lyrics3v2Field) this.fieldMap.get("IND"); field.write(file); iterator = this.fieldMap.values().iterator(); while (iterator.hasNext()) { field = (Lyrics3v2Field) iterator.next(); final String id = field.getIdentifier(); final boolean save = TagOptionSingleton.getInstance().getLyrics3SaveField(id); if ((id.equals("IND") == false) && save) { field.write(file); } } final long size; size = file.getFilePointer() - filePointer; str = Long.toString(size); for (int i = 0; i < (6 - str.length()); i++) { buffer[i] = (byte) '0'; } offset += (6 - str.length()); for (int i = 0; i < str.length(); i++) { buffer[i + offset] = (byte) str.charAt(i); } offset += str.length(); str = "LYRICS200"; for (int i = 0; i < str.length(); i++) { buffer[i + offset] = (byte) str.charAt(i); } offset += str.length(); file.write(buffer, 0, offset); if (id3v1tag != null) { id3v1tag.write(file); } } private int seekSize(final RandomAccessFile file) throws IOException { final byte[] buffer = new byte[11]; String lyricEnd; long filePointer; // check right before the ID3 1.0 tag for the lyrics tag file.seek(file.length() - 128 - 9); file.read(buffer, 0, 9); lyricEnd = new String(buffer, 0, 9); if (lyricEnd.equals("LYRICS200")) { filePointer = file.getFilePointer(); } else { // check the end of the file for a lyrics tag incase an ID3 // tag wasn't placed after it. file.seek(file.length() - 9); file.read(buffer, 0, 9); lyricEnd = new String(buffer, 0, 9); if (lyricEnd.equals("LYRICS200")) { filePointer = file.getFilePointer(); } else { return -1; } } // read the 6 bytes for the length of the tag filePointer -= (9 + 6); file.seek(filePointer); file.read(buffer, 0, 6); return Integer.parseInt(new String(buffer, 0, 6)); } public String getSongTitle() { String title = ""; Lyrics3v2Field field = getField("ETT"); if (field != null) { FieldBodyETT body = (FieldBodyETT) field.getBody(); title = body.getTitle(); } return title.trim(); } public String getLeadArtist() { String artist = ""; Lyrics3v2Field field = getField("EAR"); if (field != null) { FieldBodyEAR body = (FieldBodyEAR) field.getBody(); artist = body.getArtist(); } return artist.trim(); } public String getAlbumTitle() { String album = ""; Lyrics3v2Field field = getField("EAL"); if (field != null) { FieldBodyEAL body = (FieldBodyEAL) field.getBody(); album = body.getAlbum(); } return album.trim(); } public String getYearReleased() { throw new UnsupportedOperationException("This tag does not contain that information"); } public String getSongComment() { String additionalInformation = ""; Lyrics3v2Field field = getField("INF"); if (field != null) { FieldBodyINF body = (FieldBodyINF) field.getBody(); additionalInformation = body.getAdditionalInformation(); } return additionalInformation.trim(); } public String getSongGenre() { throw new UnsupportedOperationException("This tag does not contain that information"); } public String getTrackNumberOnAlbum() { throw new UnsupportedOperationException("This tag does not contain that information"); } public String getSongLyric() { String lyrics = ""; Lyrics3v2Field field = getField("LYR"); if (field != null) { FieldBodyLYR body = (FieldBodyLYR) field.getBody(); lyrics = body.getLyric(); } return lyrics.trim().trim(); } public String getAuthorComposer() { String author = ""; Lyrics3v2Field field = getField("AUT"); if (field != null) { FieldBodyAUT body = (FieldBodyAUT) field.getBody(); author = body.getAuthor(); } return author.trim(); } public void setSongTitle(String songTitle) { Lyrics3v2Field field = getField("ETT"); if (field == null) { field = new Lyrics3v2Field(new FieldBodyETT(songTitle.trim())); setField(field); } else { ((FieldBodyETT) field.getBody()).setTitle(songTitle.trim()); } } public void setLeadArtist(String leadArtist) { Lyrics3v2Field field = getField("EAR"); if (field == null) { field = new Lyrics3v2Field(new FieldBodyEAR(leadArtist.trim())); setField(field); } else { ((FieldBodyEAR) field.getBody()).setArtist(leadArtist.trim()); } } public void setAlbumTitle(String albumTitle) { Lyrics3v2Field field = getField("EAL"); if (field == null) { field = new Lyrics3v2Field(new FieldBodyEAL(albumTitle.trim())); setField(field); } else { ((FieldBodyEAL) field.getBody()).setAlbum(albumTitle.trim()); } } public void setYearReleased(String yearReleased) { throw new UnsupportedOperationException("This tag does not contain that information"); } public void setSongComment(String songComment) { Lyrics3v2Field field = getField("INF"); if (field == null) { field = new Lyrics3v2Field(new FieldBodyINF(songComment.trim())); setField(field); } else { ((FieldBodyINF) field.getBody()).setAdditionalInformation(songComment.trim()); } } public void setSongGenre(String songGenre) { throw new UnsupportedOperationException("This tag does not contain that information"); } public void setTrackNumberOnAlbum(String trackNumberOnAlbum) { throw new UnsupportedOperationException("This tag does not contain that information"); } public void setSongLyric(String songLyrics) { Lyrics3v2Field field = getField("LYR"); if (field == null) { field = new Lyrics3v2Field(new FieldBodyLYR(songLyrics.trim())); setField(field); } else { ((FieldBodyLYR) field.getBody()).setLyric(songLyrics.trim()); } } public void setAuthorComposer(String authorComposer) { Lyrics3v2Field field = getField("AUT"); if (field == null) { field = new Lyrics3v2Field(new FieldBodyAUT(authorComposer.trim())); setField(field); } else { ((FieldBodyAUT) field.getBody()).setAuthor(authorComposer.trim()); } } }