/*
* 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.jaudiotagger.tag.id3;
import org.jaudiotagger.FileConstants;
import org.jaudiotagger.audio.mp3.MP3File;
import org.jaudiotagger.logging.ErrorMessage;
import org.jaudiotagger.tag.*;
import org.jaudiotagger.tag.images.Artwork;
import org.jaudiotagger.tag.datatype.DataTypes;
import org.jaudiotagger.tag.id3.framebody.*;
import org.jaudiotagger.tag.images.ArtworkFactory;
import org.jaudiotagger.tag.lyrics3.AbstractLyrics3;
import org.jaudiotagger.tag.lyrics3.Lyrics3v2;
import org.jaudiotagger.tag.lyrics3.Lyrics3v2Field;
import org.jaudiotagger.tag.reference.GenreTypes;
import org.jaudiotagger.tag.reference.PictureTypes;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.util.*;
import java.util.logging.Level;
/**
* Represents an ID3v2.4 tag.
*
* @author : Paul Taylor
* @author : Eric Farng
* @version $Id: ID3v24Tag.java 976 2011-06-08 10:05:34Z paultaylor $
*/
public class ID3v24Tag extends AbstractID3v2Tag
{
protected static final String TYPE_FOOTER = "footer";
protected static final String TYPE_IMAGEENCODINGRESTRICTION = "imageEncodingRestriction";
protected static final String TYPE_IMAGESIZERESTRICTION = "imageSizeRestriction";
protected static final String TYPE_TAGRESTRICTION = "tagRestriction";
protected static final String TYPE_TAGSIZERESTRICTION = "tagSizeRestriction";
protected static final String TYPE_TEXTENCODINGRESTRICTION = "textEncodingRestriction";
protected static final String TYPE_TEXTFIELDSIZERESTRICTION = "textFieldSizeRestriction";
protected static final String TYPE_UPDATETAG = "updateTag";
protected static final String TYPE_CRCDATA = "crcdata";
protected static final String TYPE_EXPERIMENTAL = "experimental";
protected static final String TYPE_EXTENDED = "extended";
protected static final String TYPE_PADDINGSIZE = "paddingsize";
protected static final String TYPE_UNSYNCHRONISATION = "unsyncronisation";
protected static int TAG_EXT_HEADER_LENGTH = 6;
protected static int TAG_EXT_HEADER_UPDATE_LENGTH = 1;
protected static int TAG_EXT_HEADER_CRC_LENGTH = 6;
protected static int TAG_EXT_HEADER_RESTRICTION_LENGTH = 2;
protected static int TAG_EXT_HEADER_CRC_DATA_LENGTH = 5;
protected static int TAG_EXT_HEADER_RESTRICTION_DATA_LENGTH = 1;
protected static int TAG_EXT_NUMBER_BYTES_DATA_LENGTH = 1;
/**
* ID3v2.4 Header bit mask
*/
public static final int MASK_V24_UNSYNCHRONIZATION = FileConstants.BIT7;
/**
* ID3v2.4 Header bit mask
*/
public static final int MASK_V24_EXTENDED_HEADER = FileConstants.BIT6;
/**
* ID3v2.4 Header bit mask
*/
public static final int MASK_V24_EXPERIMENTAL = FileConstants.BIT5;
/**
* ID3v2.4 Header bit mask
*/
public static final int MASK_V24_FOOTER_PRESENT = FileConstants.BIT4;
/**
* ID3v2.4 Extended header bit mask
*/
public static final int MASK_V24_TAG_UPDATE = FileConstants.BIT6;
/**
* ID3v2.4 Extended header bit mask
*/
public static final int MASK_V24_CRC_DATA_PRESENT = FileConstants.BIT5;
/**
* ID3v2.4 Extended header bit mask
*/
public static final int MASK_V24_TAG_RESTRICTIONS = FileConstants.BIT4;
/**
* ID3v2.4 Extended header bit mask
*/
public static final int MASK_V24_TAG_SIZE_RESTRICTIONS = (byte) FileConstants.BIT7 | FileConstants.BIT6;
/**
* ID3v2.4 Extended header bit mask
*/
public static final int MASK_V24_TEXT_ENCODING_RESTRICTIONS = FileConstants.BIT5;
/**
* ID3v2.4 Extended header bit mask
*/
public static final int MASK_V24_TEXT_FIELD_SIZE_RESTRICTIONS = FileConstants.BIT4 | FileConstants.BIT3;
/**
* ID3v2.4 Extended header bit mask
*/
public static final int MASK_V24_IMAGE_ENCODING = FileConstants.BIT2;
/**
* ID3v2.4 Extended header bit mask
*/
public static final int MASK_V24_IMAGE_SIZE_RESTRICTIONS = FileConstants.BIT2 | FileConstants.BIT1;
/**
* ID3v2.4 Header Footer are the same as the header flags. WHY?!?! move the
* flags from thier position in 2.3??????????
*/
/**
* ID3v2.4 Header Footer bit mask
*/
public static final int MASK_V24_TAG_ALTER_PRESERVATION = FileConstants.BIT6;
/**
* ID3v2.4 Header Footer bit mask
*/
public static final int MASK_V24_FILE_ALTER_PRESERVATION = FileConstants.BIT5;
/**
* ID3v2.4 Header Footer bit mask
*/
public static final int MASK_V24_READ_ONLY = FileConstants.BIT4;
/**
* ID3v2.4 Header Footer bit mask
*/
public static final int MASK_V24_GROUPING_IDENTITY = FileConstants.BIT6;
/**
* ID3v2.4 Header Footer bit mask
*/
public static final int MASK_V24_COMPRESSION = FileConstants.BIT4;
/**
* ID3v2.4 Header Footer bit mask
*/
public static final int MASK_V24_ENCRYPTION = FileConstants.BIT3;
/**
* ID3v2.4 Header Footer bit mask
*/
public static final int MASK_V24_FRAME_UNSYNCHRONIZATION = FileConstants.BIT2;
/**
* ID3v2.4 Header Footer bit mask
*/
public static final int MASK_V24_DATA_LENGTH_INDICATOR = FileConstants.BIT1;
/**
* CRC Checksum calculated
*/
protected boolean crcDataFlag = false;
/**
* Experiemntal tag
*/
protected boolean experimental = false;
/**
* Contains extended header
*/
protected boolean extended = false;
/**
* All frames in the tag uses unsynchronisation
*/
protected boolean unsynchronization = false;
/**
* CRC Checksum
*/
protected int crcData = 0;
/**
* Contains a footer
*/
protected boolean footer = false;
/**
* Tag is an update
*/
protected boolean updateTag = false;
/**
* Tag has restrictions
*/
protected boolean tagRestriction = false;
/**
* If Set Image encoding restrictions
*
* 0 No restrictions
* 1 Images are encoded only with PNG [PNG] or JPEG [JFIF].
*/
protected byte imageEncodingRestriction = 0;
/**
* If set Image size restrictions
*
* 00 No restrictions
* 01 All images are 256x256 pixels or smaller.
* 10 All images are 64x64 pixels or smaller.
* 11 All images are exactly 64x64 pixels, unless required
* otherwise.
*/
protected byte imageSizeRestriction = 0;
/**
* If set then Tag Size Restrictions
*
* 00 No more than 128 frames and 1 MB total tag size.
* 01 No more than 64 frames and 128 KB total tag size.
* 10 No more than 32 frames and 40 KB total tag size.
* 11 No more than 32 frames and 4 KB total tag size.
*/
protected byte tagSizeRestriction = 0;
/**
* If set Text encoding restrictions
*
* 0 No restrictions
* 1 Strings are only encoded with ISO-8859-1 [ISO-8859-1] or
* UTF-8 [UTF-8].
*/
protected byte textEncodingRestriction = 0;
/**
* Tag padding
*/
protected int paddingSize = 0;
/**
* If set Text fields size restrictions
*
* 00 No restrictions
* 01 No string is longer than 1024 characters.
* 10 No string is longer than 128 characters.
* 11 No string is longer than 30 characters.
*
* Note that nothing is said about how many bytes is used to
* represent those characters, since it is encoding dependent. If a
* text frame consists of more than one string, the sum of the
* strungs is restricted as stated.
*/
protected byte textFieldSizeRestriction = 0;
public static final byte RELEASE = 2;
public static final byte MAJOR_VERSION = 4;
public static final byte REVISION = 0;
/**
* Retrieve the Release
*/
public byte getRelease()
{
return RELEASE;
}
/**
* Retrieve the Major Version
*/
public byte getMajorVersion()
{
return MAJOR_VERSION;
}
/**
* Retrieve the Revision
*/
public byte getRevision()
{
return REVISION;
}
/**
* Creates a new empty ID3v2_4 datatype.
*/
public ID3v24Tag()
{
frameMap = new LinkedHashMap();
encryptedFrameMap = new LinkedHashMap();
}
/**
* Copy primitives applicable to v2.4, this is used when cloning a v2.4 datatype
* and other objects such as v2.3 so need to check instanceof
*/
protected void copyPrimitives(AbstractID3v2Tag copyObj)
{
logger.config("Copying primitives");
super.copyPrimitives(copyObj);
if (copyObj instanceof ID3v24Tag)
{
ID3v24Tag copyObject = (ID3v24Tag) copyObj;
this.footer = copyObject.footer;
this.tagRestriction = copyObject.tagRestriction;
this.updateTag = copyObject.updateTag;
this.imageEncodingRestriction = copyObject.imageEncodingRestriction;
this.imageSizeRestriction = copyObject.imageSizeRestriction;
this.tagSizeRestriction = copyObject.tagSizeRestriction;
this.textEncodingRestriction = copyObject.textEncodingRestriction;
this.textFieldSizeRestriction = copyObject.textFieldSizeRestriction;
}
}
protected void addFrame(AbstractID3v2Frame frame)
{
try
{
if (frame instanceof ID3v24Frame)
{
copyFrameIntoMap(frame.getIdentifier(),frame);
}
else
{
ID3v24Frame newFrame = new ID3v24Frame(frame);
copyFrameIntoMap(newFrame.getIdentifier(), newFrame);
}
}
catch (InvalidFrameException ife)
{
logger.log(Level.SEVERE, "Unable to convert frame:" + frame.getIdentifier());
}
}
/*
* Copy frame into map, whilst accounting for multiple frames of same type which can occur even if there were
* not frames of the dame type in the original tag
*
* The frame already exists this shouldn't normally happen because frames
* that are allowed to be multiple don't call this method. Frames that
* aren't allowed to be multiple aren't added to hashMap in first place when
* originally added.
*
* We only want to allow one of the frames going forward but we try and merge
* all the information into the one frame. However there is a problem here that
* if we then take this, modify it and try to write back the original values
* we could lose some information although this info is probably invalid anyway.
*
* However converting some frames from tag of one version to another may
* mean that two different frames both get converted to one frame, this
* particularly applies to DateTime fields which were originally two fields
* in v2.3 but are one field in v2.4.
*/
@Override
protected void copyFrameIntoMap(String id, AbstractID3v2Frame newFrame)
{
if (frameMap.containsKey(newFrame.getIdentifier()))
{
Object o = frameMap.get(newFrame.getIdentifier());
if(o instanceof AbstractID3v2Frame)
{
//Retrieve the frame with the same id we have already loaded into the map
AbstractID3v2Frame firstFrame = (AbstractID3v2Frame) frameMap.get(newFrame.getIdentifier());
//Two different frames both converted to TDRCFrames, now if this is the case one of them
//may have actually have been created as a FrameUnsupportedBody because TDRC is only
//supported in ID3v24, but is often created in v23 tags as well together with the valid TYER
//frame OR it might be that we have two v23 frames that map to TDRC such as TYER,TIME or TDAT
if (newFrame.getBody() instanceof FrameBodyTDRC)
{
if (firstFrame.getBody() instanceof FrameBodyTDRC)
{
logger.finest("Modifying frame in map:" + newFrame.getIdentifier());
FrameBodyTDRC body = (FrameBodyTDRC) firstFrame.getBody();
FrameBodyTDRC newBody = (FrameBodyTDRC) newFrame.getBody();
//#304:Check for NullPointer, just ignore this frame
if(newBody.getOriginalID()==null)
{
return;
}
//Just add the data to the frame
if (newBody.getOriginalID().equals(ID3v23Frames.FRAME_ID_V3_TYER))
{
body.setYear(newBody.getYear());
}
else if (newBody.getOriginalID().equals(ID3v23Frames.FRAME_ID_V3_TDAT))
{
body.setDate(newBody.getDate());
body.setMonthOnly(newBody.isMonthOnly());
}
else if (newBody.getOriginalID().equals(ID3v23Frames.FRAME_ID_V3_TIME))
{
body.setTime(newBody.getTime());
body.setHoursOnly(newBody.isHoursOnly());
}
body.setObjectValue(DataTypes.OBJ_TEXT,body.getFormattedText());
}
// The first frame was a TDRC frame that was not really allowed, this new frame was probably a
// valid frame such as TYER which has been converted to TDRC, replace the firstframe with this frame
else if (firstFrame.getBody() instanceof FrameBodyUnsupported)
{
frameMap.put(newFrame.getIdentifier(), newFrame);
}
else
{
//we just lose this frame, weve already got one with the correct id.
//TODO may want to store this somewhere
logger.warning("Found duplicate TDRC frame in invalid situation,discarding:" + newFrame.getIdentifier());
}
}
else
{
List<AbstractID3v2Frame> list = new ArrayList<AbstractID3v2Frame>();
list.add(firstFrame);
list.add(newFrame);
frameMap.put(newFrame.getIdentifier(), list);
}
}
else
{
List<AbstractID3v2Frame> list = (List)o;
list.add(newFrame);
}
}
else
{
frameMap.put(newFrame.getIdentifier(), newFrame);
}
}
/**
* Copy Constructor, creates a new ID3v2_4 Tag based on another ID3v2_4 Tag
* @param copyObject
*/
public ID3v24Tag(ID3v24Tag copyObject)
{
logger.config("Creating tag from another tag of same type");
copyPrimitives(copyObject);
copyFrames(copyObject);
}
/**
* Creates a new ID3v2_4 datatype based on another (non 2.4) tag
*
* @param mp3tag
*/
public ID3v24Tag(AbstractTag mp3tag)
{
logger.config("Creating tag from a tag of a different version");
frameMap = new LinkedHashMap();
encryptedFrameMap = new LinkedHashMap();
if (mp3tag != null)
{
//Should use simpler copy constructor
if ((mp3tag instanceof ID3v24Tag))
{
throw new UnsupportedOperationException("Copy Constructor not called. Please type cast the argument");
}
/* If we get a tag, we want to convert to id3v2_4
* both id3v1 and lyrics3 convert to this type
* id3v1 needs to convert to id3v2_4 before converting to lyrics3
*/
else if (mp3tag instanceof AbstractID3v2Tag)
{
this.setLoggingFilename(((AbstractID3v2Tag)mp3tag).getLoggingFilename());
copyPrimitives((AbstractID3v2Tag) mp3tag);
copyFrames((AbstractID3v2Tag) mp3tag);
}
//IDv1
else if (mp3tag instanceof ID3v1Tag)
{
// convert id3v1 tags.
ID3v1Tag id3tag = (ID3v1Tag) mp3tag;
ID3v24Frame newFrame;
AbstractID3v2FrameBody newBody;
if (id3tag.title.length() > 0)
{
newBody = new FrameBodyTIT2((byte) 0, id3tag.title);
newFrame = new ID3v24Frame(ID3v24Frames.FRAME_ID_TITLE);
newFrame.setBody(newBody);
frameMap.put(newFrame.getIdentifier(), newFrame);
}
if (id3tag.artist.length() > 0)
{
newBody = new FrameBodyTPE1((byte) 0, id3tag.artist);
newFrame = new ID3v24Frame(ID3v24Frames.FRAME_ID_ARTIST);
newFrame.setBody(newBody);
frameMap.put(newFrame.getIdentifier(), newFrame);
}
if (id3tag.album.length() > 0)
{
newBody = new FrameBodyTALB((byte) 0, id3tag.album);
newFrame = new ID3v24Frame(ID3v24Frames.FRAME_ID_ALBUM);
newFrame.setBody(newBody);
frameMap.put(newFrame.getIdentifier(), newFrame);
}
if (id3tag.year.length() > 0)
{
newBody = new FrameBodyTDRC((byte) 0, id3tag.year);
newFrame = new ID3v24Frame(ID3v24Frames.FRAME_ID_YEAR);
newFrame.setBody(newBody);
frameMap.put(newFrame.getIdentifier(), newFrame);
}
if (id3tag.comment.length() > 0)
{
newBody = new FrameBodyCOMM((byte) 0, "ENG", "", id3tag.comment);
newFrame = new ID3v24Frame(ID3v24Frames.FRAME_ID_COMMENT);
newFrame.setBody(newBody);
frameMap.put(newFrame.getIdentifier(), newFrame);
}
if (((id3tag.genre & ID3v1Tag.BYTE_TO_UNSIGNED) >= 0) && ((id3tag.genre & ID3v1Tag.BYTE_TO_UNSIGNED) != ID3v1Tag.BYTE_TO_UNSIGNED))
{
Integer genreId = id3tag.genre & ID3v1Tag.BYTE_TO_UNSIGNED;
String genre = "(" + genreId + ") " + GenreTypes.getInstanceOf().getValueForId(genreId);
newBody = new FrameBodyTCON((byte) 0, genre);
newFrame = new ID3v24Frame(ID3v24Frames.FRAME_ID_GENRE);
newFrame.setBody(newBody);
frameMap.put(newFrame.getIdentifier(), newFrame);
}
if (mp3tag instanceof ID3v11Tag)
{
ID3v11Tag id3tag2 = (ID3v11Tag) mp3tag;
if (id3tag2.track > 0)
{
newBody = new FrameBodyTRCK((byte) 0, Byte.toString(id3tag2.track));
newFrame = new ID3v24Frame(ID3v24Frames.FRAME_ID_TRACK);
newFrame.setBody(newBody);
frameMap.put(newFrame.getIdentifier(), newFrame);
}
}
}
//Lyrics 3
else if (mp3tag instanceof AbstractLyrics3)
{
//Put the conversion stuff in the individual frame code.
Lyrics3v2 lyric;
if (mp3tag instanceof Lyrics3v2)
{
lyric = new Lyrics3v2((Lyrics3v2) mp3tag);
}
else
{
lyric = new Lyrics3v2(mp3tag);
}
Iterator<Lyrics3v2Field> iterator = lyric.iterator();
Lyrics3v2Field field;
ID3v24Frame newFrame;
while (iterator.hasNext())
{
try
{
field = iterator.next();
newFrame = new ID3v24Frame(field);
frameMap.put(newFrame.getIdentifier(), newFrame);
}
catch (InvalidTagException ex)
{
logger.warning("Unable to convert Lyrics3 to v24 Frame:Frame Identifier");
}
}
}
}
}
/**
* Creates a new ID3v2_4 datatype.
*
* @param buffer
* @param loggingFilename
* @throws TagException
*/
public ID3v24Tag(ByteBuffer buffer, String loggingFilename) throws TagException
{
frameMap = new LinkedHashMap();
encryptedFrameMap = new LinkedHashMap();
setLoggingFilename(loggingFilename);
this.read(buffer);
}
/**
* Creates a new ID3v2_4 datatype.
*
* @param buffer
* @throws TagException
* @deprecated use {@link #ID3v24Tag(ByteBuffer,String)} instead
*/
public ID3v24Tag(ByteBuffer buffer) throws TagException
{
this(buffer, "");
}
/**
* @return identifier
*/
public String getIdentifier()
{
return "ID3v2.40";
}
/**
* Return tag size based upon the sizes of the frames rather than the physical
* no of bytes between start of ID3Tag and start of Audio Data.
*
* @return size
*/
public int getSize()
{
int size = TAG_HEADER_LENGTH;
if (extended)
{
size += TAG_EXT_HEADER_LENGTH;
if (updateTag)
{
size += TAG_EXT_HEADER_UPDATE_LENGTH;
}
if (crcDataFlag)
{
size += TAG_EXT_HEADER_CRC_LENGTH;
}
if (tagRestriction)
{
size += TAG_EXT_HEADER_RESTRICTION_LENGTH;
}
}
size += super.getSize();
logger.finer("Tag Size is" + size);
return size;
}
/**
* @param obj
* @return equality
*/
public boolean equals(Object obj)
{
if (!(obj instanceof ID3v24Tag))
{
return false;
}
ID3v24Tag object = (ID3v24Tag) obj;
if (this.footer != object.footer)
{
return false;
}
if (this.imageEncodingRestriction != object.imageEncodingRestriction)
{
return false;
}
if (this.imageSizeRestriction != object.imageSizeRestriction)
{
return false;
}
if (this.tagRestriction != object.tagRestriction)
{
return false;
}
if (this.tagSizeRestriction != object.tagSizeRestriction)
{
return false;
}
if (this.textEncodingRestriction != object.textEncodingRestriction)
{
return false;
}
if (this.textFieldSizeRestriction != object.textFieldSizeRestriction)
{
return false;
}
return this.updateTag == object.updateTag && super.equals(obj);
}
/**
* Read header flags
* <p/>
* <p>Log info messages for falgs that have been set and log warnings when bits have been set for unknown flags</p>
*
* @param byteBuffer
* @throws TagException
*/
private void readHeaderFlags(ByteBuffer byteBuffer) throws TagException
{
//Flags
byte flags = byteBuffer.get();
unsynchronization = (flags & MASK_V24_UNSYNCHRONIZATION) != 0;
extended = (flags & MASK_V24_EXTENDED_HEADER) != 0;
experimental = (flags & MASK_V24_EXPERIMENTAL) != 0;
footer = (flags & MASK_V24_FOOTER_PRESENT) != 0;
//Not allowable/Unknown Flags
if ((flags & FileConstants.BIT3) != 0)
{
logger.warning(ErrorMessage.ID3_INVALID_OR_UNKNOWN_FLAG_SET.getMsg(getLoggingFilename(), FileConstants.BIT3));
}
if ((flags & FileConstants.BIT2) != 0)
{
logger.warning(ErrorMessage.ID3_INVALID_OR_UNKNOWN_FLAG_SET.getMsg(getLoggingFilename(), FileConstants.BIT2));
}
if ((flags & FileConstants.BIT1) != 0)
{
logger.warning(ErrorMessage.ID3_INVALID_OR_UNKNOWN_FLAG_SET.getMsg(getLoggingFilename(), FileConstants.BIT1));
}
if ((flags & FileConstants.BIT0) != 0)
{
logger.warning(ErrorMessage.ID3_INVALID_OR_UNKNOWN_FLAG_SET.getMsg(getLoggingFilename(), FileConstants.BIT0));
}
if (isUnsynchronization())
{
logger.config(ErrorMessage.ID3_TAG_UNSYNCHRONIZED.getMsg(getLoggingFilename()));
}
if (extended)
{
logger.config(ErrorMessage.ID3_TAG_EXTENDED.getMsg(getLoggingFilename()));
}
if (experimental)
{
logger.config(ErrorMessage.ID3_TAG_EXPERIMENTAL.getMsg(getLoggingFilename()));
}
if (footer)
{
logger.warning(ErrorMessage.ID3_TAG_FOOTER.getMsg(getLoggingFilename()));
}
}
/**
* Read the optional extended header
*
* @param byteBuffer
* @param size
* @throws org.jaudiotagger.tag.InvalidTagException
*/
private void readExtendedHeader(ByteBuffer byteBuffer, int size) throws InvalidTagException
{
byte[] buffer;
// int is 4 bytes.
int extendedHeaderSize = byteBuffer.getInt();
// the extended header must be at least 6 bytes
if (extendedHeaderSize <= TAG_EXT_HEADER_LENGTH)
{
throw new InvalidTagException(ErrorMessage.ID3_EXTENDED_HEADER_SIZE_TOO_SMALL.getMsg(getLoggingFilename(), extendedHeaderSize));
}
//Number of bytes
byteBuffer.get();
// Read the extended flag bytes
byte extFlag = byteBuffer.get();
updateTag = (extFlag & MASK_V24_TAG_UPDATE) != 0;
crcDataFlag = (extFlag & MASK_V24_CRC_DATA_PRESENT) != 0;
tagRestriction = (extFlag & MASK_V24_TAG_RESTRICTIONS) != 0;
// read the length byte if the flag is set
// this tag should always be zero but just in case
// read this information.
if (updateTag)
{
byteBuffer.get();
}
//CRC-32
if (crcDataFlag)
{
// the CRC has a variable length
byteBuffer.get();
buffer = new byte[TAG_EXT_HEADER_CRC_DATA_LENGTH];
byteBuffer.get(buffer, 0, TAG_EXT_HEADER_CRC_DATA_LENGTH);
crcData = 0;
for (int i = 0; i < TAG_EXT_HEADER_CRC_DATA_LENGTH; i++)
{
crcData <<= 8;
crcData += buffer[i];
}
}
//Tag Restriction
if (tagRestriction)
{
byteBuffer.get();
buffer = new byte[1];
byteBuffer.get(buffer, 0, 1);
tagSizeRestriction = (byte) ((buffer[0] & MASK_V24_TAG_SIZE_RESTRICTIONS) >> 6);
textEncodingRestriction = (byte) ((buffer[0] & MASK_V24_TEXT_ENCODING_RESTRICTIONS) >> 5);
textFieldSizeRestriction = (byte) ((buffer[0] & MASK_V24_TEXT_FIELD_SIZE_RESTRICTIONS) >> 3);
imageEncodingRestriction = (byte) ((buffer[0] & MASK_V24_IMAGE_ENCODING) >> 2);
imageSizeRestriction = (byte) (buffer[0] & MASK_V24_IMAGE_SIZE_RESTRICTIONS);
}
}
/**
* {@inheritDoc}
*/
@Override
public void read(ByteBuffer byteBuffer) throws TagException
{
int size;
byte[] buffer;
if (!seek(byteBuffer))
{
throw new TagNotFoundException(getLoggingFilename() + ":" + getIdentifier() + " tag not found");
}
logger.config(getLoggingFilename() + ":" + "Reading ID3v24 tag");
readHeaderFlags(byteBuffer);
// Read the size, this is size of tag apart from tag header
size = ID3SyncSafeInteger.bufferToValue(byteBuffer);
logger.config(getLoggingFilename() + ":" + "Reading tag from file size set in header is" + size);
if (extended)
{
readExtendedHeader(byteBuffer, size);
}
//Note if there was an extended header the size value has padding taken
//off so we dont search it.
readFrames(byteBuffer, size);
}
/**
* Read frames from tag
* @param byteBuffer
* @param size
*/
protected void readFrames(ByteBuffer byteBuffer, int size)
{
logger.finest(getLoggingFilename() + ":" + "Start of frame body at" + byteBuffer.position());
//Now start looking for frames
ID3v24Frame next;
frameMap = new LinkedHashMap();
encryptedFrameMap = new LinkedHashMap();
//Read the size from the Tag Header
this.fileReadSize = size;
// Read the frames until got to upto the size as specified in header
logger.finest(getLoggingFilename() + ":" + "Start of frame body at:" + byteBuffer.position() + ",frames data size is:" + size);
while (byteBuffer.position() <= size)
{
String id;
try
{
//Read Frame
logger.finest(getLoggingFilename() + ":" + "looking for next frame at:" + byteBuffer.position());
next = new ID3v24Frame(byteBuffer, getLoggingFilename());
id = next.getIdentifier();
loadFrameIntoMap(id, next);
}
//Found Padding, no more frames
catch (PaddingException ex)
{
logger.config(getLoggingFilename() + ":Found padding starting at:" + byteBuffer.position());
break;
}
//Found Empty Frame
catch (EmptyFrameException ex)
{
logger.warning(getLoggingFilename() + ":" + "Empty Frame:" + ex.getMessage());
this.emptyFrameBytes += TAG_HEADER_LENGTH;
}
catch (InvalidFrameIdentifierException ifie)
{
logger.config(getLoggingFilename() + ":" + "Invalid Frame Identifier:" + ifie.getMessage());
this.invalidFrames++;
//Don't try and find any more frames
break;
}
//Problem trying to find frame
catch (InvalidFrameException ife)
{
logger.warning(getLoggingFilename() + ":" + "Invalid Frame:" + ife.getMessage());
this.invalidFrames++;
//Don't try and find any more frames
break;
}
//Failed reading frame but may just have invalid data but correct length so lets carry on
//in case we can read the next frame
catch(InvalidDataTypeException idete)
{
logger.warning(getLoggingFilename() + ":Corrupt Frame:" + idete.getMessage());
this.invalidFrames++;
continue;
}
}
}
/**
* Write the ID3 header to the ByteBuffer.
* <p/>
* TODO Calculate the CYC Data Check
* TODO Reintroduce Extended Header
*
* @param padding is the size of the padding
* @param size is the size of the body data
* @return ByteBuffer
* @throws IOException
*/
private ByteBuffer writeHeaderToBuffer(int padding, int size) throws IOException
{
//This would only be set if every frame in tag has been unsynchronized, I only unsychronize frames
//that need it, in any case I have been advised not to set it even then.
unsynchronization = false;
// Flags,currently we never calculate the CRC
// and if we dont calculate them cant keep orig values. Tags are not
// experimental and we never create extended header to keep things simple.
extended = false;
experimental = false;
footer = false;
// Create Header Buffer,allocate maximum possible size for the header
ByteBuffer headerBuffer = ByteBuffer.allocate(TAG_HEADER_LENGTH);
//TAGID
headerBuffer.put(TAG_ID);
//Major Version
headerBuffer.put(getMajorVersion());
//Minor Version
headerBuffer.put(getRevision());
//Flags
byte flagsByte = 0;
if (isUnsynchronization())
{
flagsByte |= MASK_V24_UNSYNCHRONIZATION;
}
if (extended)
{
flagsByte |= MASK_V24_EXTENDED_HEADER;
}
if (experimental)
{
flagsByte |= MASK_V24_EXPERIMENTAL;
}
if (footer)
{
flagsByte |= MASK_V24_FOOTER_PRESENT;
}
headerBuffer.put(flagsByte);
//Size As Recorded in Header, don't include the main header length
//Additional Header Size,(for completeness we never actually write the extended header, or footer)
int additionalHeaderSize = 0;
if (extended)
{
additionalHeaderSize += TAG_EXT_HEADER_LENGTH;
if (updateTag)
{
additionalHeaderSize += TAG_EXT_HEADER_UPDATE_LENGTH;
}
if (crcDataFlag)
{
additionalHeaderSize += TAG_EXT_HEADER_CRC_LENGTH;
}
if (tagRestriction)
{
additionalHeaderSize += TAG_EXT_HEADER_RESTRICTION_LENGTH;
}
}
//Size As Recorded in Header, don't include the main header length
headerBuffer.put(ID3SyncSafeInteger.valueToBuffer(padding + size + additionalHeaderSize));
//Write Extended Header
ByteBuffer extHeaderBuffer = null;
if (extended)
{
//Write Extended Header Size
int extendedSize = TAG_EXT_HEADER_LENGTH;
if (updateTag)
{
extendedSize += TAG_EXT_HEADER_UPDATE_LENGTH;
}
if (crcDataFlag)
{
extendedSize += TAG_EXT_HEADER_CRC_LENGTH;
}
if (tagRestriction)
{
extendedSize += TAG_EXT_HEADER_RESTRICTION_LENGTH;
}
extHeaderBuffer = ByteBuffer.allocate(extendedSize);
extHeaderBuffer.putInt(extendedSize);
//Write Number of flags Byte
extHeaderBuffer.put((byte) TAG_EXT_NUMBER_BYTES_DATA_LENGTH);
//Write Extended Flags
byte extFlag = 0;
if (updateTag)
{
extFlag |= MASK_V24_TAG_UPDATE;
}
if (crcDataFlag)
{
extFlag |= MASK_V24_CRC_DATA_PRESENT;
}
if (tagRestriction)
{
extFlag |= MASK_V24_TAG_RESTRICTIONS;
}
extHeaderBuffer.put(extFlag);
//Write Update Data
if (updateTag)
{
extHeaderBuffer.put((byte) 0);
}
//Write CRC Data
if (crcDataFlag)
{
extHeaderBuffer.put((byte) TAG_EXT_HEADER_CRC_DATA_LENGTH);
extHeaderBuffer.put((byte) 0);
extHeaderBuffer.putInt(crcData);
}
//Write Tag Restriction
if (tagRestriction)
{
extHeaderBuffer.put((byte) TAG_EXT_HEADER_RESTRICTION_DATA_LENGTH);
//todo not currently setting restrictions
extHeaderBuffer.put((byte) 0);
}
}
if (extHeaderBuffer != null)
{
extHeaderBuffer.flip();
headerBuffer.put(extHeaderBuffer);
}
headerBuffer.flip();
return headerBuffer;
}
/**
* {@inheritDoc}
*/
@Override
public void write(File file, long audioStartLocation) throws IOException
{
setLoggingFilename(file.getName());
logger.config("Writing tag to file:"+getLoggingFilename());
//Write Body Buffer
byte[] bodyByteBuffer = writeFramesToBuffer().toByteArray();
//Calculate Tag Size including Padding
int sizeIncPadding = calculateTagSize(bodyByteBuffer.length + TAG_HEADER_LENGTH, (int) audioStartLocation);
//Calculate padding bytes required
int padding = sizeIncPadding - (bodyByteBuffer.length + TAG_HEADER_LENGTH);
ByteBuffer headerBuffer = writeHeaderToBuffer(padding, bodyByteBuffer.length);
writeBufferToFile(file, headerBuffer, bodyByteBuffer, padding, sizeIncPadding, audioStartLocation);
}
/**
* {@inheritDoc}
*/
@Override
public void write(WritableByteChannel channel) throws IOException
{
logger.config("Writing tag to channel");
byte[] bodyByteBuffer = writeFramesToBuffer().toByteArray();
ByteBuffer headerBuffer = writeHeaderToBuffer(0, bodyByteBuffer.length);
channel.write(headerBuffer);
channel.write(ByteBuffer.wrap(bodyByteBuffer));
}
/**
* Display the tag in an XMLFormat
*/
public void createStructure()
{
MP3File.getStructureFormatter().openHeadingElement(TYPE_TAG, getIdentifier());
super.createStructureHeader();
//Header
MP3File.getStructureFormatter().openHeadingElement(TYPE_HEADER, "");
MP3File.getStructureFormatter().addElement(TYPE_UNSYNCHRONISATION, this.isUnsynchronization());
MP3File.getStructureFormatter().addElement(TYPE_CRCDATA, this.crcData);
MP3File.getStructureFormatter().addElement(TYPE_EXPERIMENTAL, this.experimental);
MP3File.getStructureFormatter().addElement(TYPE_EXTENDED, this.extended);
MP3File.getStructureFormatter().addElement(TYPE_PADDINGSIZE, this.paddingSize);
MP3File.getStructureFormatter().addElement(TYPE_FOOTER, this.footer);
MP3File.getStructureFormatter().addElement(TYPE_IMAGEENCODINGRESTRICTION, this.paddingSize);
MP3File.getStructureFormatter().addElement(TYPE_IMAGESIZERESTRICTION, this.imageSizeRestriction);
MP3File.getStructureFormatter().addElement(TYPE_TAGRESTRICTION, this.tagRestriction);
MP3File.getStructureFormatter().addElement(TYPE_TAGSIZERESTRICTION, this.tagSizeRestriction);
MP3File.getStructureFormatter().addElement(TYPE_TEXTFIELDSIZERESTRICTION, this.textFieldSizeRestriction);
MP3File.getStructureFormatter().addElement(TYPE_TEXTENCODINGRESTRICTION, this.textEncodingRestriction);
MP3File.getStructureFormatter().addElement(TYPE_UPDATETAG, this.updateTag);
MP3File.getStructureFormatter().closeHeadingElement(TYPE_HEADER);
//Body
super.createStructureBody();
MP3File.getStructureFormatter().closeHeadingElement(TYPE_TAG);
}
/**
* Are all frame swithin this tag unsynchronized
* <p/>
* <p>Because synchronization occurs at the frame level it is not normally desirable to unsynchronize all frames
* and hence this flag is not normally set.
*
* @return are all frames within the tag unsynchronized
*/
public boolean isUnsynchronization()
{
return unsynchronization;
}
/**
* Create a new frame with the specified frameid
*
* @param id
* @return
*/
public ID3v24Frame createFrame(String id)
{
return new ID3v24Frame(id);
}
/**
* Create Frame for Id3 Key
* <p/>
* Only textual data supported at the moment, should only be used with frames that
* support a simple string argument.
*
* @param id3Key
* @param value
* @return
* @throws KeyNotFoundException
* @throws FieldDataInvalidException
*/
public TagField createField(ID3v24FieldKey id3Key, String value) throws KeyNotFoundException, FieldDataInvalidException
{
if (id3Key == null)
{
throw new KeyNotFoundException();
}
return super.doCreateTagField(new FrameAndSubId(id3Key.getFrameId(), id3Key.getSubId()), value);
}
/**
* Retrieve the first value that exists for this id3v24key
*
* @param id3v24FieldKey
* @return
* @throws org.jaudiotagger.tag.KeyNotFoundException
*/
public String getFirst(ID3v24FieldKey id3v24FieldKey) throws KeyNotFoundException
{
if (id3v24FieldKey == null)
{
throw new KeyNotFoundException();
}
FrameAndSubId frameAndSubId = new FrameAndSubId(id3v24FieldKey.getFrameId(), id3v24FieldKey.getSubId());
if (id3v24FieldKey == ID3v24FieldKey.TRACK)
{
AbstractID3v2Frame frame = getFirstField(frameAndSubId.getFrameId());
return String.valueOf(((FrameBodyTRCK)frame.getBody()).getTrackNo());
}
else if (id3v24FieldKey == ID3v24FieldKey.TRACK_TOTAL)
{
AbstractID3v2Frame frame = getFirstField(frameAndSubId.getFrameId());
return String.valueOf(((FrameBodyTRCK)frame.getBody()).getTrackTotal());
}
else if (id3v24FieldKey == ID3v24FieldKey.DISC_NO)
{
AbstractID3v2Frame frame = getFirstField(frameAndSubId.getFrameId());
return String.valueOf(((FrameBodyTPOS)frame.getBody()).getDiscNo());
}
else if (id3v24FieldKey == ID3v24FieldKey.DISC_TOTAL)
{
AbstractID3v2Frame frame = getFirstField(frameAndSubId.getFrameId());
return String.valueOf(((FrameBodyTPOS)frame.getBody()).getDiscTotal());
}
else
{
return super.doGetValueAtIndex(frameAndSubId, 0);
}
}
/**
* Delete fields with this id3v24FieldKey
*
* @param id3v24FieldKey
* @throws org.jaudiotagger.tag.KeyNotFoundException
*/
public void deleteField(ID3v24FieldKey id3v24FieldKey) throws KeyNotFoundException
{
if (id3v24FieldKey == null)
{
throw new KeyNotFoundException();
}
super.doDeleteTagField(new FrameAndSubId(id3v24FieldKey.getFrameId(), id3v24FieldKey.getSubId()));
}
/**
* Delete fields with this (frame) id
* @param id
*/
public void deleteField(String id)
{
super.doDeleteTagField(new FrameAndSubId(id,null));
}
protected FrameAndSubId getFrameAndSubIdFromGenericKey(FieldKey genericKey)
{
ID3v24FieldKey id3v24FieldKey = ID3v24Frames.getInstanceOf().getId3KeyFromGenericKey(genericKey);
if (id3v24FieldKey == null)
{
throw new KeyNotFoundException("Unable to find key for "+genericKey.name());
}
return new FrameAndSubId(id3v24FieldKey.getFrameId(), id3v24FieldKey.getSubId());
}
protected ID3Frames getID3Frames()
{
return ID3v24Frames.getInstanceOf();
}
/**
* @return comparator used to order frames in preferred order for writing to file
* so that most important frames are written first.
*/
public Comparator getPreferredFrameOrderComparator()
{
return ID3v24PreferredFrameOrderComparator.getInstanceof();
}
public List<Artwork> getArtworkList()
{
List<TagField> coverartList = getFields(FieldKey.COVER_ART);
List<Artwork> artworkList = new ArrayList<Artwork>(coverartList.size());
for (TagField next : coverartList)
{
FrameBodyAPIC coverArt = (FrameBodyAPIC) ((AbstractID3v2Frame) next).getBody();
Artwork artwork = ArtworkFactory.getNew();
artwork.setMimeType(coverArt.getMimeType());
artwork.setPictureType(coverArt.getPictureType());
if (coverArt.isImageUrl())
{
artwork.setLinked(true);
artwork.setImageUrl(coverArt.getImageUrl());
}
else
{
artwork.setBinaryData(coverArt.getImageData());
}
artworkList.add(artwork);
}
return artworkList;
}
public TagField createField(Artwork artwork) throws FieldDataInvalidException
{
AbstractID3v2Frame frame = createFrame(getFrameAndSubIdFromGenericKey(FieldKey.COVER_ART).getFrameId());
FrameBodyAPIC body = (FrameBodyAPIC) frame.getBody();
if(!artwork.isLinked())
{
body.setObjectValue(DataTypes.OBJ_PICTURE_DATA, artwork.getBinaryData());
body.setObjectValue(DataTypes.OBJ_PICTURE_TYPE, artwork.getPictureType());
body.setObjectValue(DataTypes.OBJ_MIME_TYPE, artwork.getMimeType());
body.setObjectValue(DataTypes.OBJ_DESCRIPTION, "");
return frame;
}
else
{
try
{
body.setObjectValue(DataTypes.OBJ_PICTURE_DATA,artwork.getImageUrl().getBytes("ISO-8859-1"));
}
catch(UnsupportedEncodingException uoe)
{
throw new RuntimeException(uoe.getMessage());
}
body.setObjectValue(DataTypes.OBJ_PICTURE_TYPE, artwork.getPictureType());
body.setObjectValue(DataTypes.OBJ_MIME_TYPE, FrameBodyAPIC.IMAGE_IS_URL);
body.setObjectValue(DataTypes.OBJ_DESCRIPTION, "");
return frame;
}
}
/**
* Create Artwork
*
* @param data
* @param mimeType of the image
* @see PictureTypes
* @return
*/
public TagField createArtworkField(byte[] data, String mimeType)
{
AbstractID3v2Frame frame = createFrame(getFrameAndSubIdFromGenericKey(FieldKey.COVER_ART).getFrameId());
FrameBodyAPIC body = (FrameBodyAPIC) frame.getBody();
body.setObjectValue(DataTypes.OBJ_PICTURE_DATA, data);
body.setObjectValue(DataTypes.OBJ_PICTURE_TYPE, PictureTypes.DEFAULT_ID);
body.setObjectValue(DataTypes.OBJ_MIME_TYPE, mimeType);
body.setObjectValue(DataTypes.OBJ_DESCRIPTION, "");
return frame;
}
}
|