/** * @author : Paul Taylor * @author : Eric Farng * * Version @version:$Id$ * * MusicTag Copyright (C)2003,2004 * * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation; either version 2.1 of the License, * or (at your option) any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along with this library; if not, * you can get a copy from http://www.opensource.org/licenses/lgpl-license.php or write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ package org.jaudiotagger.audio.mp3; import org.jaudiotagger.audio.AudioFile; import org.jaudiotagger.audio.exceptions.CannotWriteException; import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException; import org.jaudiotagger.audio.exceptions.ReadOnlyFileException; import org.jaudiotagger.logging.*; import org.jaudiotagger.tag.Tag; import org.jaudiotagger.tag.TagException; import org.jaudiotagger.tag.TagNotFoundException; import org.jaudiotagger.tag.TagOptionSingleton; import org.jaudiotagger.tag.id3.*; import org.jaudiotagger.tag.lyrics3.AbstractLyrics3; import org.jaudiotagger.tag.reference.ID3V2Version; import java.io.*; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.logging.Level; /** * This class represents a physical MP3 File */ public class MP3File extends AudioFile { private static final int MINIMUM_FILESIZE = 150; protected static AbstractTagDisplayFormatter tagFormatter; /** * the ID3v2 tag that this file contains. */ private AbstractID3v2Tag id3v2tag = null; /** * Representation of the idv2 tag as a idv24 tag */ private ID3v24Tag id3v2Asv24tag = null; /** * The Lyrics3 tag that this file contains. */ private AbstractLyrics3 lyrics3tag = null; /** * The ID3v1 tag that this file contains. */ private ID3v1Tag id3v1tag = null; /** * Creates a new empty MP3File datatype that is not associated with a * specific file. */ public MP3File() { } /** * Creates a new MP3File datatype and parse the tag from the given filename. * * @param filename MP3 file * @throws IOException on any I/O error * @throws TagException on any exception generated by this library. * @throws org.jaudiotagger.audio.exceptions.ReadOnlyFileException * @throws org.jaudiotagger.audio.exceptions.InvalidAudioFrameException */ public MP3File(String filename) throws IOException, TagException, ReadOnlyFileException, InvalidAudioFrameException { this(new File(filename)); } /* Load ID3V1tag if exists */ public static final int LOAD_IDV1TAG = 2; /* Load ID3V2tag if exists */ public static final int LOAD_IDV2TAG = 4; /** * This option is currently ignored */ public static final int LOAD_LYRICS3 = 8; public static final int LOAD_ALL = LOAD_IDV1TAG | LOAD_IDV2TAG | LOAD_LYRICS3; /** * Creates a new MP3File dataType and parse the tag from the given file * Object, files must be writable to use this constructor. * * @param file MP3 file * @param loadOptions decide what tags to load * @throws IOException on any I/O error * @throws TagException on any exception generated by this library. * @throws org.jaudiotagger.audio.exceptions.ReadOnlyFileException * @throws org.jaudiotagger.audio.exceptions.InvalidAudioFrameException */ public MP3File(File file, int loadOptions) throws IOException, TagException, ReadOnlyFileException, InvalidAudioFrameException { this(file, loadOptions, false); } /** * Read v1 tag * * @param file * @param newFile * @param loadOptions * @throws IOException */ private void readV1Tag(File file, RandomAccessFile newFile, int loadOptions) throws IOException { if ((loadOptions & LOAD_IDV1TAG) != 0) { logger.finer("Attempting to read id3v1tags"); try { id3v1tag = new ID3v11Tag(newFile, file.getName()); } catch (TagNotFoundException ex) { logger.config("No ids3v11 tag found"); } try { if (id3v1tag == null) { id3v1tag = new ID3v1Tag(newFile, file.getName()); } } catch (TagNotFoundException ex) { logger.config("No id3v1 tag found"); } } } /** * Read V2tag if exists * * TODO:shouldn'timer we be handing TagExceptions:when will they be thrown * * @param file * @param loadOptions * @throws IOException * @throws TagException */ private void readV2Tag(File file, int loadOptions, int startByte) throws IOException, TagException { //We know where the actual Audio starts so load all the file from start to that point into //a buffer then we can read the IDv2 information without needing any more File I/O if (startByte >= AbstractID3v2Tag.TAG_HEADER_LENGTH) { logger.finer("Attempting to read id3v2tags"); FileInputStream fis = null; FileChannel fc = null; ByteBuffer bb; try { fis = new FileInputStream(file); fc = fis.getChannel(); // avoid using fc.map method since it does not work on Android ICS and JB. Bug report: https://code.google.com/p/android/issues/detail?id=53637 // bb = fc.map(FileChannel.MapMode.READ_ONLY,0,startByte); bb = ByteBuffer.allocate(startByte); fc.read(bb, 0); } finally { if (fc != null) { fc.close(); } if (fis != null) { fis.close(); } } try { bb.rewind(); if ((loadOptions & LOAD_IDV2TAG) != 0) { logger.config("Attempting to read id3v2tags"); try { this.setID3v2Tag(new ID3v24Tag(bb, file.getName())); } catch (TagNotFoundException ex) { logger.config("No id3v24 tag found"); } try { if (id3v2tag == null) { this.setID3v2Tag(new ID3v23Tag(bb, file.getName())); } } catch (TagNotFoundException ex) { logger.config("No id3v23 tag found"); } try { if (id3v2tag == null) { this.setID3v2Tag(new ID3v22Tag(bb, file.getName())); } } catch (TagNotFoundException ex) { logger.config("No id3v22 tag found"); } } } finally { bb.clear(); } } else { logger.config("Not enough room for valid id3v2 tag:" + startByte); } } /** * Read lyrics3 Tag * * TODO:not working * * @param file * @param newFile * @param loadOptions * @throws IOException */ private void readLyrics3Tag(File file, RandomAccessFile newFile, int loadOptions) throws IOException { /*if ((loadOptions & LOAD_LYRICS3) != 0) { try { lyrics3tag = new Lyrics3v2(newFile); } catch (TagNotFoundException ex) { } try { if (lyrics3tag == null) { lyrics3tag = new Lyrics3v1(newFile); } } catch (TagNotFoundException ex) { } } */ } /** * * @param startByte * @param endByte * @return * @throws Exception * * @return true if all the bytes between in the file between startByte and endByte are null, false * otherwise */ private boolean isFilePortionNull(int startByte, int endByte) throws IOException { logger.config("Checking file portion:" + Hex.asHex(startByte) + ":" + Hex.asHex(endByte)); FileInputStream fis=null; FileChannel fc=null; try { fis = new FileInputStream(file); fc = fis.getChannel(); fc.position(startByte); ByteBuffer bb = ByteBuffer.allocateDirect(endByte - startByte); fc.read(bb); while(bb.hasRemaining()) { if(bb.get()!=0) { return false; } } } finally { if (fc != null) { fc.close(); } if (fis != null) { fis.close(); } } return true; } /** * Regets the audio header starting from start of file, and write appropriate logging to indicate * potential problem to user. * * @param startByte * @param firstHeaderAfterTag * @return * @throws IOException * @throws InvalidAudioFrameException */ private MP3AudioHeader checkAudioStart(long startByte, MP3AudioHeader firstHeaderAfterTag) throws IOException, InvalidAudioFrameException { MP3AudioHeader headerOne; MP3AudioHeader headerTwo; logger.warning(ErrorMessage.MP3_ID3TAG_LENGTH_INCORRECT.getMsg(file.getPath(), Hex.asHex(startByte), Hex.asHex(firstHeaderAfterTag.getMp3StartByte()))); //because we cant agree on start location we reread the audioheader from the start of the file, at least //this way we cant overwrite the audio although we might overwrite part of the tag if we write this file //back later headerOne = new MP3AudioHeader(file, 0); logger.config("Checking from start:" + headerOne); //Although the id3 tag size appears to be incorrect at least we have found the same location for the start //of audio whether we start searching from start of file or at the end of the alleged of file so no real //problem if (firstHeaderAfterTag.getMp3StartByte() == headerOne.getMp3StartByte()) { logger.config(ErrorMessage.MP3_START_OF_AUDIO_CONFIRMED.getMsg(file.getPath(), Hex.asHex(headerOne.getMp3StartByte()))); return firstHeaderAfterTag; } else { //We get a different value if read from start, can'timer guarantee 100% correct lets do some more checks logger.config((ErrorMessage.MP3_RECALCULATED_POSSIBLE_START_OF_MP3_AUDIO.getMsg(file.getPath(), Hex.asHex(headerOne.getMp3StartByte())))); //Same frame count so probably both audio headers with newAudioHeader being the first one if (firstHeaderAfterTag.getNumberOfFrames() == headerOne.getNumberOfFrames()) { logger.warning((ErrorMessage.MP3_RECALCULATED_START_OF_MP3_AUDIO.getMsg(file.getPath(), Hex.asHex(headerOne.getMp3StartByte())))); return headerOne; } //If the size reported by the tag header is a little short and there is only nulls between the recorded value //and the start of the first audio found then we stick with the original header as more likely that currentHeader //DataInputStream not really a header if(isFilePortionNull((int) startByte,(int) firstHeaderAfterTag.getMp3StartByte())) { return firstHeaderAfterTag; } //Skip to the next header (header 2, counting from start of file) headerTwo = new MP3AudioHeader(file, headerOne.getMp3StartByte() + headerOne.mp3FrameHeader.getFrameLength()); //It matches the header we found when doing the original search from after the ID3Tag therefore it //seems that newAudioHeader was a false match and the original header was correct if (headerTwo.getMp3StartByte() == firstHeaderAfterTag.getMp3StartByte()) { logger.warning((ErrorMessage.MP3_START_OF_AUDIO_CONFIRMED.getMsg(file.getPath(), Hex.asHex(firstHeaderAfterTag.getMp3StartByte())))); return firstHeaderAfterTag; } //It matches the frameCount the header we just found so lends weight to the fact that the audio does indeed start at new header //however it maybe that neither are really headers and just contain the same data being misrepresented as headers. if (headerTwo.getNumberOfFrames() == headerOne.getNumberOfFrames()) { logger.warning((ErrorMessage.MP3_RECALCULATED_START_OF_MP3_AUDIO.getMsg(file.getPath(), Hex.asHex(headerOne.getMp3StartByte())))); return headerOne; } ///Doesnt match the frameCount lets go back to the original header else { logger.warning((ErrorMessage.MP3_RECALCULATED_START_OF_MP3_AUDIO.getMsg(file.getPath(), Hex.asHex(firstHeaderAfterTag.getMp3StartByte())))); return firstHeaderAfterTag; } } } /** * Creates a new MP3File dataType and parse the tag from the given file * Object, files can be opened read only if required. * * @param file MP3 file * @param loadOptions decide what tags to load * @param readOnly causes the files to be opened readonly * @throws IOException on any I/O error * @throws TagException on any exception generated by this library. * @throws org.jaudiotagger.audio.exceptions.ReadOnlyFileException * @throws org.jaudiotagger.audio.exceptions.InvalidAudioFrameException */ public MP3File(File file, int loadOptions, boolean readOnly) throws IOException, TagException, ReadOnlyFileException, InvalidAudioFrameException { RandomAccessFile newFile = null; try { this.file = file; //Check File accessibility newFile = checkFilePermissions(file, readOnly); //Read ID3v2 tag size (if tag exists) to allow audioHeader parsing to skip over tag long tagSizeReportedByHeader = AbstractID3v2Tag.getV2TagSizeIfExists(file); logger.config("TagHeaderSize:" + Hex.asHex(tagSizeReportedByHeader)); audioHeader = new MP3AudioHeader(file, tagSizeReportedByHeader); //If the audio header is not straight after the end of the tag then search from start of file if (tagSizeReportedByHeader != ((MP3AudioHeader) audioHeader).getMp3StartByte()) { logger.config("First header found after tag:" + audioHeader); audioHeader = checkAudioStart(tagSizeReportedByHeader, (MP3AudioHeader) audioHeader); } //Read v1 tags (if any) readV1Tag(file, newFile, loadOptions); //Read v2 tags (if any) readV2Tag(file, loadOptions, (int)((MP3AudioHeader) audioHeader).getMp3StartByte()); //If we have a v2 tag use that, if we do not but have v1 tag use that //otherwise use nothing //TODO:if have both should we merge //rather than just returning specific ID3v22 tag, would it be better to return v24 version ? if (this.getID3v2Tag() != null) { tag = this.getID3v2Tag(); } else if (id3v1tag != null) { tag = id3v1tag; } } finally { if (newFile != null) { newFile.close(); } } } /** * Used by tags when writing to calculate the location of the music file * * @param file * @return the location within the file that the audio starts * @throws java.io.IOException * @throws org.jaudiotagger.audio.exceptions.InvalidAudioFrameException */ public long getMP3StartByte(File file) throws InvalidAudioFrameException, IOException { try { //Read ID3v2 tag size (if tag exists) to allow audio header parsing to skip over tag long startByte = AbstractID3v2Tag.getV2TagSizeIfExists(file); MP3AudioHeader audioHeader = new MP3AudioHeader(file, startByte); if (startByte != audioHeader.getMp3StartByte()) { logger.config("First header found after tag:" + audioHeader); audioHeader = checkAudioStart(startByte, audioHeader); } return audioHeader.getMp3StartByte(); } catch (InvalidAudioFrameException iafe) { throw iafe; } catch (IOException ioe) { throw ioe; } } /** * Extracts the raw ID3v2 tag data into a file. * * This provides access to the raw data before manipulation, the data is written from the start of the file * to the start of the Audio Data. This is primarily useful for manipulating corrupted tags that are not * (fully) loaded using the standard methods. * * @param outputFile to write the data to * @return * @throws TagNotFoundException * @throws IOException */ public File extractID3v2TagDataIntoFile(File outputFile) throws TagNotFoundException, IOException { int startByte = (int) ((MP3AudioHeader) audioHeader).getMp3StartByte(); if (startByte >= 0) { //Read byte into buffer FileInputStream fis = new FileInputStream(file); FileChannel fc = fis.getChannel(); ByteBuffer bb = ByteBuffer.allocate(startByte); fc.read(bb); //Write bytes to outputFile FileOutputStream out = new FileOutputStream(outputFile); out.write(bb.array()); out.close(); fc.close(); fis.close(); return outputFile; } throw new TagNotFoundException("There is no ID3v2Tag data in this file"); } /** * Return audio header * @return */ public MP3AudioHeader getMP3AudioHeader() { return (MP3AudioHeader) getAudioHeader(); } /** * Returns true if this datatype contains an <code>Id3v1</code> tag * * @return true if this datatype contains an <code>Id3v1</code> tag */ public boolean hasID3v1Tag() { return (id3v1tag != null); } /** * Returns true if this datatype contains an <code>Id3v2</code> tag * * @return true if this datatype contains an <code>Id3v2</code> tag */ public boolean hasID3v2Tag() { return (id3v2tag != null); } /** * Returns true if this datatype contains a <code>Lyrics3</code> tag * TODO disabled until Lyrics3 fixed * @return true if this datatype contains a <code>Lyrics3</code> tag */ /* public boolean hasLyrics3Tag() { return (lyrics3tag != null); } */ /** * Creates a new MP3File datatype and parse the tag from the given file * Object. * * @param file MP3 file * @throws IOException on any I/O error * @throws TagException on any exception generated by this library. * @throws org.jaudiotagger.audio.exceptions.ReadOnlyFileException * @throws org.jaudiotagger.audio.exceptions.InvalidAudioFrameException */ public MP3File(File file) throws IOException, TagException, ReadOnlyFileException, InvalidAudioFrameException { this(file, LOAD_ALL); } /** * Sets the ID3v1(_1)tag to the tag provided as an argument. * * @param id3v1tag */ public void setID3v1Tag(ID3v1Tag id3v1tag) { logger.config("setting tagv1:v1 tag"); this.id3v1tag = id3v1tag; } public void setID3v1Tag(Tag id3v1tag) { logger.config("setting tagv1:v1 tag"); this.id3v1tag = (ID3v1Tag) id3v1tag; } /** * Sets the <code>ID3v1</code> tag for this dataType. A new * <code>ID3v1_1</code> dataType is created from the argument and then used * here. * * @param mp3tag Any MP3Tag dataType can be used and will be converted into a * new ID3v1_1 dataType. */ public void setID3v1Tag(AbstractTag mp3tag) { logger.config("setting tagv1:abstract"); id3v1tag = new ID3v11Tag(mp3tag); } /** * Returns the <code>ID3v1</code> tag for this dataType. * * @return the <code>ID3v1</code> tag for this dataType */ public ID3v1Tag getID3v1Tag() { return id3v1tag; } /** * Sets the <code>ID3v2</code> tag for this dataType. A new * <code>ID3v2_4</code> dataType is created from the argument and then used * here. * * @param mp3tag Any MP3Tag dataType can be used and will be converted into a * new ID3v2_4 dataType. */ public void setID3v2Tag(AbstractTag mp3tag) { id3v2tag = new ID3v24Tag(mp3tag); } /** * Sets the v2 tag to the v2 tag provided as an argument. * Also store a v24 version of tag as v24 is the interface to be used * when talking with client applications. * * @param id3v2tag */ public void setID3v2Tag(AbstractID3v2Tag id3v2tag) { this.id3v2tag = id3v2tag; if (id3v2tag instanceof ID3v24Tag) { this.id3v2Asv24tag = (ID3v24Tag) this.id3v2tag; } else { this.id3v2Asv24tag = new ID3v24Tag(id3v2tag); } } /** * Set v2 tag ,don'timer need to set v24 tag because saving * * @param id3v2tag */ //TODO temp its rather messy public void setID3v2TagOnly(AbstractID3v2Tag id3v2tag) { this.id3v2tag = id3v2tag; this.id3v2Asv24tag = null; } /** * Returns the <code>ID3v2</code> tag for this datatype. * * @return the <code>ID3v2</code> tag for this datatype */ public AbstractID3v2Tag getID3v2Tag() { return id3v2tag; } /** * @return a representation of tag as v24 */ public ID3v24Tag getID3v2TagAsv24() { return id3v2Asv24tag; } /** * Sets the <code>Lyrics3</code> tag for this dataType. A new * <code>Lyrics3v2</code> dataType is created from the argument and then * * used here. * * @param mp3tag Any MP3Tag dataType can be used and will be converted into a * new Lyrics3v2 dataType. */ /* public void setLyrics3Tag(AbstractTag mp3tag) { lyrics3tag = new Lyrics3v2(mp3tag); } */ /** * * * @param lyrics3tag */ /* public void setLyrics3Tag(AbstractLyrics3 lyrics3tag) { this.lyrics3tag = lyrics3tag; } */ /** * Returns the <code>ID3v1</code> tag for this datatype. * * @return the <code>ID3v1</code> tag for this datatype */ /* public AbstractLyrics3 getLyrics3Tag() { return lyrics3tag; } */ /** * Remove tag from file * * @param mp3tag * @throws FileNotFoundException * @throws IOException */ public void delete(AbstractTag mp3tag) throws FileNotFoundException, IOException { RandomAccessFile raf = new RandomAccessFile(this.file, "rw"); mp3tag.delete(raf); raf.close(); if(mp3tag instanceof ID3v1Tag) { id3v1tag=null; } if(mp3tag instanceof AbstractID3v2Tag) { id3v2tag=null; } } /** * Saves the tags in this dataType to the file referred to by this dataType. * * @throws IOException on any I/O error * @throws TagException on any exception generated by this library. */ public void save() throws IOException, TagException { save(this.file); } /** * Overridden for compatibility with merged code * * @throws CannotWriteException */ public void commit() throws CannotWriteException { try { save(); } catch (IOException ioe) { throw new CannotWriteException(ioe); } catch (TagException te) { throw new CannotWriteException(te); } } /** * Check can write to file * * @param file * @throws IOException */ public void precheck(File file) throws IOException { if (!file.exists()) { logger.severe(ErrorMessage.GENERAL_WRITE_FAILED_BECAUSE_FILE_NOT_FOUND.getMsg(file.getName())); throw new IOException(ErrorMessage.GENERAL_WRITE_FAILED_BECAUSE_FILE_NOT_FOUND.getMsg(file.getName())); } if (!file.canWrite()) { logger.severe(ErrorMessage.GENERAL_WRITE_FAILED.getMsg(file.getName())); throw new IOException(ErrorMessage.GENERAL_WRITE_FAILED.getMsg(file.getName())); } if (file.length() <= MINIMUM_FILESIZE) { logger.severe(ErrorMessage.GENERAL_WRITE_FAILED_BECAUSE_FILE_IS_TOO_SMALL.getMsg(file.getName())); throw new IOException(ErrorMessage.GENERAL_WRITE_FAILED_BECAUSE_FILE_IS_TOO_SMALL.getMsg(file.getName())); } } /** * Saves the tags in this dataType to the file argument. It will be saved as * TagConstants.MP3_FILE_SAVE_WRITE * * @param fileToSave file to save the this dataTypes tags to * @throws FileNotFoundException if unable to find file * @throws IOException on any I/O error */ public void save(File fileToSave) throws IOException { //Ensure we are dealing with absolute filepaths not relative ones File file = fileToSave.getAbsoluteFile(); logger.config("Saving : " + file.getPath()); //Checks before starting write precheck(file); RandomAccessFile rfile = null; try { //ID3v2 Tag if (TagOptionSingleton.getInstance().isId3v2Save()) { if (id3v2tag == null) { rfile = new RandomAccessFile(file, "rw"); (new ID3v24Tag()).delete(rfile); (new ID3v23Tag()).delete(rfile); (new ID3v22Tag()).delete(rfile); logger.config("Deleting ID3v2 tag:"+file.getName()); rfile.close(); } else { logger.config("Writing ID3v2 tag:"+file.getName()); final MP3AudioHeader mp3AudioHeader = (MP3AudioHeader) this.getAudioHeader(); final long mp3StartByte = mp3AudioHeader.getMp3StartByte(); final long newMp3StartByte = id3v2tag.write(file, mp3StartByte); if (mp3StartByte != newMp3StartByte) { logger.config("New mp3 start byte: " + newMp3StartByte); mp3AudioHeader.setMp3StartByte(newMp3StartByte); } } } rfile = new RandomAccessFile(file, "rw"); //Lyrics 3 Tag if (TagOptionSingleton.getInstance().isLyrics3Save()) { if (lyrics3tag != null) { lyrics3tag.write(rfile); } } //ID3v1 tag if (TagOptionSingleton.getInstance().isId3v1Save()) { logger.config("Processing ID3v1"); if (id3v1tag == null) { logger.config("Deleting ID3v1"); (new ID3v1Tag()).delete(rfile); } else { logger.config("Saving ID3v1"); id3v1tag.write(rfile); } } } catch (FileNotFoundException ex) { logger.log(Level.SEVERE, ErrorMessage.GENERAL_WRITE_FAILED_BECAUSE_FILE_NOT_FOUND.getMsg(file.getName()), ex); throw ex; } catch (IOException iex) { logger.log(Level.SEVERE, ErrorMessage.GENERAL_WRITE_FAILED_BECAUSE.getMsg(file.getName(), iex.getMessage()), iex); throw iex; } catch (RuntimeException re) { logger.log(Level.SEVERE, ErrorMessage.GENERAL_WRITE_FAILED_BECAUSE.getMsg(file.getName(), re.getMessage()), re); throw re; } finally { if (rfile != null) { rfile.close(); } } } /** * Displays MP3File Structure */ public String displayStructureAsXML() { createXMLStructureFormatter(); tagFormatter.openHeadingElement("file", this.getFile().getAbsolutePath()); if (this.getID3v1Tag() != null) { this.getID3v1Tag().createStructure(); } if (this.getID3v2Tag() != null) { this.getID3v2Tag().createStructure(); } tagFormatter.closeHeadingElement("file"); return tagFormatter.toString(); } /** * Displays MP3File Structure */ public String displayStructureAsPlainText() { createPlainTextStructureFormatter(); tagFormatter.openHeadingElement("file", this.getFile().getAbsolutePath()); if (this.getID3v1Tag() != null) { this.getID3v1Tag().createStructure(); } if (this.getID3v2Tag() != null) { this.getID3v2Tag().createStructure(); } tagFormatter.closeHeadingElement("file"); return tagFormatter.toString(); } private static void createXMLStructureFormatter() { tagFormatter = new XMLTagDisplayFormatter(); } private static void createPlainTextStructureFormatter() { tagFormatter = new PlainTextTagDisplayFormatter(); } public static AbstractTagDisplayFormatter getStructureFormatter() { return tagFormatter; } /** * Set the Tag * * If the parameter tag is a v1tag then the v1 tag is set if v2tag then the v2tag. * * @param tag */ public void setTag(Tag tag) { this.tag = tag; if (tag instanceof ID3v1Tag) { setID3v1Tag((ID3v1Tag) tag); } else { setID3v2Tag((AbstractID3v2Tag) tag); } } /** Create Default Tag * * @return */ @Override public Tag createDefaultTag() { if(TagOptionSingleton.getInstance().getID3V2Version()==ID3V2Version.ID3_V24) { return new ID3v24Tag(); } else if(TagOptionSingleton.getInstance().getID3V2Version()==ID3V2Version.ID3_V23) { return new ID3v23Tag(); } else if(TagOptionSingleton.getInstance().getID3V2Version()==ID3V2Version.ID3_V22) { return new ID3v22Tag(); } //Default in case not set somehow return new ID3v24Tag(); } /** * Convert tag from current version to another as specified by id3V2Version * * @return */ public Tag convertTag(Tag tag, ID3V2Version id3V2Version) { if(tag instanceof ID3v24Tag) { switch(id3V2Version) { case ID3_V22: return new ID3v22Tag((ID3v24Tag)tag); case ID3_V23: return new ID3v23Tag((ID3v24Tag)tag); case ID3_V24: return tag; } } else if(tag instanceof ID3v23Tag) { switch(id3V2Version) { case ID3_V22: return new ID3v22Tag((ID3v23Tag)tag); case ID3_V23: return tag; case ID3_V24: return new ID3v24Tag((ID3v23Tag)tag); } } else if(tag instanceof ID3v22Tag) { switch(id3V2Version) { case ID3_V22: return tag; case ID3_V23: return new ID3v23Tag((ID3v22Tag)tag); case ID3_V24: return new ID3v24Tag((ID3v22Tag)tag); } } return tag; } /** * Overidden to only consider ID3v2 Tag * * @return */ @Override public Tag getTagOrCreateDefault() { Tag tag = getID3v2Tag(); if(tag==null) { return createDefaultTag(); } return tag; } /** * Get the ID3v2 tag and convert to preferred version or if the file doesn'timer have one at all * create a default tag of preferred version and set it. The file may already contain a ID3v1 tag but because * this is not terribly useful the v1tag is not considered for this problem. * * @return */ @Override public Tag getTagAndConvertOrCreateAndSetDefault() { Tag tag = getTagOrCreateDefault(); tag=convertTag(tag, TagOptionSingleton.getInstance().getID3V2Version()); setTag(tag); return tag; } }