📄 abstractid3v2tag.java
字号:
* Checks to see if the file contains an ID3tag and if so return its size as reported in
* the tag header and return the size of the tag (including header), if no such tag exists return
* zero.
*
* @param file
* @return the end of the tag in the file or zero if no tag exists.
*/
public static long getV2TagSizeIfExists(File file) throws IOException
{
FileInputStream fis = null;
FileChannel fc = null;
ByteBuffer bb = null;
try
{
//Files
fis = new FileInputStream(file);
fc = fis.getChannel();
//Read possible Tag header Byte Buffer
bb = ByteBuffer.allocate(TAG_HEADER_LENGTH);
fc.read(bb);
bb.flip();
if (bb.limit() < (TAG_HEADER_LENGTH))
{
return 0;
}
}
finally
{
if (fc != null)
{
fc.close();
}
if (fis != null)
{
fis.close();
}
}
//ID3 identifier
byte[] tagIdentifier = new byte[FIELD_TAGID_LENGTH];
bb.get(tagIdentifier, 0, FIELD_TAGID_LENGTH);
if (!(Arrays.equals(tagIdentifier, TAG_ID)))
{
return 0;
}
//Is it valid Major Version
byte majorVersion = bb.get();
if ((majorVersion != ID3v22Tag.MAJOR_VERSION) && (majorVersion != ID3v23Tag.MAJOR_VERSION) && (majorVersion != ID3v24Tag.MAJOR_VERSION))
{
return 0;
}
//Skip Minor Version
bb.get();
//Skip Flags
bb.get();
//Get size as recorded in frame header
int frameSize = ID3SyncSafeInteger.bufferToValue(bb);
//add header size to frame size
frameSize += TAG_HEADER_LENGTH;
return frameSize;
}
/**
* Does a tag of the correct version exist in this file.
*
* @param byteBuffer to search through
* @return true if tag exists.
*/
public boolean seek(ByteBuffer byteBuffer)
{
byteBuffer.rewind();
logger.info("ByteBuffer pos:" + byteBuffer.position() + ":limit" + byteBuffer.limit() + ":cap" + byteBuffer.capacity());
byte[] tagIdentifier = new byte[FIELD_TAGID_LENGTH];
byteBuffer.get(tagIdentifier, 0, FIELD_TAGID_LENGTH);
if (!(Arrays.equals(tagIdentifier, TAG_ID)))
{
return false;
}
//Major Version
if (byteBuffer.get() != getMajorVersion())
{
return false;
}
//Minor Version
if (byteBuffer.get() != getRevision())
{
return false;
}
return true;
}
/**
* This method determines the total tag size taking into account
* where the audio file starts, the size of the tagging data and
* user options for defining how tags should shrink or grow.
*/
protected int calculateTagSize(int tagSize, int audioStart)
{
/** We can fit in the tag so no adjustments required */
if (tagSize <= audioStart)
{
return audioStart;
}
/** There is not enough room as we need to move the audio file we might
* as well increase it more than neccessary for future changes
*/
return tagSize + TAG_SIZE_INCREMENT;
}
/**
* Adjust the length of the padding at the beginning of the MP3 file, this is only called when there is currently
* not enough space before the start of the audio to write the tag.
* <p/>
* A new file will be created with enough size to fit the <code>ID3v2</code> tag.
* The old file will be deleted, and the new file renamed.
*
* @param paddingSize This is total size required to store tag before audio
* @param file The file to adjust the padding length of
* @throws FileNotFoundException if the file exists but is a directory
* rather than a regular file or cannot be opened for any other
* reason
* @throws IOException on any I/O error
*/
public void adjustPadding(File file, int paddingSize, long audioStart) throws FileNotFoundException, IOException
{
logger.finer("Need to move audio file to accomodate tag");
FileChannel fcIn;
FileChannel fcOut;
// Create buffer holds the neccessary padding
ByteBuffer paddingBuffer = ByteBuffer.wrap(new byte[paddingSize]);
// Create Temporary File and write channel
File paddedFile = File.createTempFile("temp", ".mp3", file.getParentFile());
fcOut = new FileOutputStream(paddedFile).getChannel();
//Create read channel from original file
fcIn = new FileInputStream(file).getChannel();
//Write padding to new file (this is where the tag will be written to later)
long written = (long) fcOut.write(paddingBuffer);
//Write rest of file starting from audio
logger.finer("Copying:" + (file.length() - audioStart) + "bytes");
//if the amount to be copied is very large we split into 10MB lumps to try and avoid
//out of memory errors
long audiolength = file.length() - audioStart;
if (audiolength <= MAXIMUM_WRITABLE_CHUNK_SIZE)
{
long written2 = fcIn.transferTo(audioStart, audiolength, fcOut);
logger.finer("Written padding:" + written + " Data:" + written2);
if (written2 != audiolength)
{
throw new RuntimeException("Problem adjusting padding, expecting to write:" + audiolength + ":only wrote:" + written2);
}
}
else
{
long noOfChunks = audiolength / MAXIMUM_WRITABLE_CHUNK_SIZE;
long lastChunkSize = audiolength % MAXIMUM_WRITABLE_CHUNK_SIZE;
long written2 = 0;
for (int i = 0; i < noOfChunks; i++)
{
written2 += fcIn.transferTo(audioStart + (i * MAXIMUM_WRITABLE_CHUNK_SIZE), MAXIMUM_WRITABLE_CHUNK_SIZE, fcOut);
//Try and recover memory as quick as possible
Runtime.getRuntime().gc();
}
written2 += fcIn.transferTo(audioStart + (noOfChunks * MAXIMUM_WRITABLE_CHUNK_SIZE), lastChunkSize, fcOut);
logger.finer("Written padding:" + written + " Data:" + written2);
if (written2 != audiolength)
{
throw new RuntimeException("Problem adjusting padding in large file, expecting to write:" + audiolength + ":only wrote:" + written2);
}
}
//Store original modification time
long lastModified = file.lastModified();
//Close Channels
fcIn.close();
fcOut.close();
//Delete original File
file.delete();
//Rename temporary file and set modification time to original time.
paddedFile.renameTo(file);
paddedFile.setLastModified(lastModified);
}
/**
* Add frame to HashMap used when converting between tag versions, take into account
* occurences when two frame may both map to a single frame when converting between
* versions
* <p/>
* TODO the logic here is messy and seems to be specific to date fields only when it
* was intended to be generic.
*/
protected void copyFrameIntoMap(String id, AbstractID3v2Frame newFrame)
{
/* The frame already exists this shouldnt normally happen because frames
* that are allowed to be multiple don't call this method. Frames that
* arent 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
* particulary applies to DateTime fields which were originally two fields
* in v2.3 but are one field in v2.4.
*/
if (frameMap.containsKey(newFrame.getIdentifier()))
{
//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
*/
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();
//Just add the data to the frame
if (newBody.getOriginalID().equals(ID3v23Frames.FRAME_ID_V3_TYER))
{
body.setYear(newBody.getText());
}
else if (newBody.getOriginalID().equals(ID3v23Frames.FRAME_ID_V3_TDAT))
{
body.setDate(newBody.getText());
}
else if (newBody.getOriginalID().equals(ID3v23Frames.FRAME_ID_V3_TIME))
{
body.setTime(newBody.getText());
}
else if (newBody.getOriginalID().equals(ID3v23Frames.FRAME_ID_V3_TRDA))
{
body.setReco(newBody.getText());
}
}
/* 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
{
logger.warning("Found duplicate frame in invalid situation,discarding:" + newFrame.getIdentifier());
}
}
else
//Just add frame to map
{
logger.finest("Adding frame to map:" + newFrame.getIdentifier());
frameMap.put(newFrame.getIdentifier(), newFrame);
}
}
/**
* Decides what to with the frame that has just be read from file.
* If the frame is an allowable duplicate frame and is a duplicate we add all
* frames into an ArrayList and add the Arraylist to the hashMap. if not allowed
* to be duplicate we store bytes in the duplicateBytes variable.
*/
protected void loadFrameIntoMap(String frameId, AbstractID3v2Frame next)
{
if ((ID3v24Frames.getInstanceOf().isMultipleAllowed(frameId)) || (ID3v23Frames.getInstanceOf().isMultipleAllowed(frameId)) || (ID3v22Frames.getInstanceOf().isMultipleAllowed(frameId)))
{
//If a frame already exists of this type
if (frameMap.containsKey(frameId))
{
Object o = frameMap.get(frameId);
if (o instanceof ArrayList)
{
ArrayList multiValues = (ArrayList) o;
multiValues.add(next);
logger.finer("Adding Multi Frame(1)" + frameId);
}
else
{
ArrayList multiValues = new ArrayList();
multiValues.add(o);
multiValues.add(next);
frameMap.put(frameId, multiValues);
logger.finer("Adding Multi Frame(2)" + frameId);
}
}
else
{
logger.finer("Adding Multi FrameList(3)" + frameId);
frameMap.put(frameId, next);
}
}
//If duplicate frame just stores it somewhere else
else if (frameMap.containsKey(frameId))
{
logger.warning("Duplicate Frame" + frameId);
this.duplicateFrameId += (frameId + "; ");
this.duplicateBytes += ((AbstractID3v2Frame) frameMap.get(frameId)).getSize();
}
else
{
logger.finer("Adding Frame" + frameId);
frameMap.put(frameId, next);
}
}
/**
* Return tag size based upon the sizes of the tags rather than the physical
* no of bytes between start of ID3Tag and start of Audio Data.Should be extended
* by subclasses to include header.
*
* @return size of the tag
*/
public int getSize()
{
int size = 0;
Iterator iterator = frameMap.values().iterator();
AbstractID3v2Frame frame;
while (iterator.hasNext())
{
Object o = iterator.next();
if (o instanceof AbstractID3v2Frame)
{
frame = (AbstractID3v2Frame) o;
size += frame.getSize();
}
else
{
ArrayList multiFrames = (ArrayList) o;
for (ListIterator li = multiFrames.listIterator(); li.hasNext();)
{
frame = (AbstractID3v2Frame) li.next();
size += frame.getSize();
}
}
}
return size;
}
/**
* Write all the frames to the byteArrayOutputStream
* <p/>
* <p>Currently Write all frames, defaults to the order in which they were loaded, newly
* created frames will be at end of tag.
*
* @return ByteBuffer Contains all the frames written within the tag ready for writing to file
* @throws IOException
*/
//TODO there is a preferred tag order mentioned in spec, e.g ufid first
protected ByteArrayOutputStream writeFramesToBuffer() throws IOException
{
//Increases as is required
ByteArrayOutputStream bodyBuffer = new ByteArrayOutputStream();
AbstractID3v2Frame frame;
Iterator iterator;
iterator = frameMap.values().iterator();
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -