package com.limegroup.gnutella.hashing;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import org.jaudiotagger.audio.AudioFile;
import org.jaudiotagger.audio.AudioFileIO;
import org.jaudiotagger.audio.exceptions.CannotReadException;
import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException;
import org.jaudiotagger.audio.exceptions.ReadOnlyFileException;
import org.jaudiotagger.audio.mp3.MP3File;
import org.jaudiotagger.tag.TagException;
import org.limewire.util.StringUtils;
/**
* Locates the beginning and end of the audio portion of an mp3 file. This
* checks for ID3v1.0-ID3v2.4 tags, LYRICS3 tags, and APE tags. Tags
* located at the end of the audio stream are explicitely checked for. As
* a result, any padding added to the end of the audio stream is considered
* part of the audio portion of the file.
*/
class MP3NonMetaDataHasher extends NonMetaDataHasher {
/** Begining String of a LYRICS3 tag. */
private static final String LYRICSBEGIN = "LYRICSBEGIN";
/** Ending String of a LYRICS3v1.0 tag. */
private static final String LYRICSEND_V1 = "LYRICSEND";
/** Ending String of a LYRICS3v2.0 tag. */
private static final String LYRICSEND_V2 = "LYRICS200";
/** Begining/Ending String of a APE tag. */
private static final String APETAG = "APETAGEX";
private final File file;
MP3NonMetaDataHasher(File file) {
this.file = file;
}
/**
* Returns the start position of the audio portion of this mp3.
*/
@Override
public long getStartPosition() throws IOException {
try {
AudioFile audioFile = AudioFileIO.read(file);
if(!(audioFile instanceof MP3File)) {
throw new IOException("Cannot cast to a MP3File");
}
MP3File mp3File = (MP3File) audioFile;
return mp3File.getMP3StartByte(mp3File.getFile());
} catch (InvalidAudioFrameException e) {
throw new IOException(e);
} catch (CannotReadException e) {
throw new IOException(e);
} catch (TagException e) {
throw new IOException(e);
} catch (ReadOnlyFileException e) {
throw new IOException(e);
}
}
/**
* Checks the end of an Mp3File for various metadata tags. These
* include ID3v1.0, ID3v1.1, LYRIC3V1, LYRIC3V2, and APE tags. Other
* metadata may exist there but this is the most common.
*
* ID3v1.x tags are always checked first and if they exist will always appear
* last within the file. LYRIC3 tag or APE tags may exist before an ID3v1.x
* tag or if an ID3v1.x tag doesn't exist, at the end of the file. We assume
* LYRIC3 and APE tags can never coexist. Both are extremely rare and will
* hardly ever exist to begin with.
*
* Returns the position within the file where the audio or padding preceding
* any of these tags. If no tags are located, returns the length of the audio file.
*/
@Override
public long getEndPosition() throws IOException {
AudioFile audioFile;
try {
audioFile = AudioFileIO.read(file);
} catch (CannotReadException e) {
throw new IOException(e);
} catch (TagException e) {
throw new IOException(e);
} catch (ReadOnlyFileException e) {
throw new IOException(e);
} catch (InvalidAudioFrameException e) {
throw new IOException(e);
}
if(!(audioFile instanceof MP3File)) {
throw new IOException("Cannot cast to a MP3File");
}
MP3File mp3File = (MP3File) audioFile;
long fileLength = file.length();
int offset = 0;
// id3 tag v1.0 and v1.1 will always will be last and is always 128 bytes.
if(mp3File.hasID3v1Tag()) {
LOG.debug("found ID3v1 tag");
offset = 128;
fileLength -= 128;
}
// This buffer is large enough to locate the existense of all footer
// tags. Extra IO may be needed if an APE or LYRICS3 footer tag is
// located but both of these tags are extremely rare.
ByteBuffer buffer = ByteBuffer.allocate(32);
fillBuffer(buffer, mp3File.getFile(), offset);
if(buffer.limit() < 32) {
throw new IOException("Couldn't fill buffer while parsing footers");
}
// check for the LYRICSv1.0 & LYRICSv2.0 tag
// these tags will precede ID3v1.x tag or EOF
String lyricsTag = getLyricsFooterTag(buffer);
if(LYRICSEND_V1.equals(lyricsTag)) {
LOG.debug("found LYRICS3 footer tag");
// 5100 is the maximum length of a LYRICSv1.0 tag, whether the 9 byte footer
// tag is included in this in undefined
ByteBuffer newByteBuffer = ByteBuffer.allocate(5100 + 9);
fillBuffer(newByteBuffer, mp3File.getFile(), offset);
fileLength -= getSizeLyricsTagV1(newByteBuffer);
} else if(LYRICSEND_V2.equals(lyricsTag)) {
LOG.debug("found LYRICS3v2 footer tag");
fileLength -= getSizeLyricsTagV2(buffer, offset, mp3File);
}
// check for APE tag, this will precede EOF, or ID3v1.x
if(containsAPETag(buffer)) {
LOG.debug("found APE footer tag");
fileLength -= getSizeAPETag(buffer, offset, mp3File);
}
return fileLength;
}
/**
* LYRICS3 tags is an all text descriptor that contains lyrical information
* about an mp3. LYRICS3 tags are located at the end of an mp3 or if an
* ID3v1.x tag exists, immediately preceding the ID3v1.x tag.
*
* There are currently two different version of LRYICS3 tags, v1 and v2. The
* end of a LYRICS3 tag will contain a 9 byte string. LYRICS3v1.0 footer tags
* can be identified with the String "LYRICSEND". LYRICS3v2.0 footer tags
* can be identified with the String "LYRICS200".
*
* This returns the String comprised of the 9 bytes immediately preceding
* EOF or if an ID3v1.x tag exists, the 9 bytes preceding that tag.
*
* http://www.id3.org/Lyrics3
*/
private static String getLyricsFooterTag(ByteBuffer buffer) {
byte[] bytes = new byte[9];
buffer.position(buffer.limit() - 9);
buffer.get(bytes);
return StringUtils.getASCIIString(bytes);
}
/**
* Lyrics3v1 tags are located at the end of an mp3 file or right before
* an ID3v1.x tag. The last 9 bytes are LRYICSEND. If this tag is found,
* search back 5100 bytes and begin scanning for the LYRICSBEGIN tag.
* LYRICSBEGIN is considered the start of the Lyrics3v1 tag.
*
* http://www.id3.org/Lyrics3
*/
private static long getSizeLyricsTagV1(ByteBuffer buffer) throws IOException {
byte[] bytes = new byte[11];
for(int i = 0; i < buffer.limit() - bytes.length; i++) {
buffer.position(i);
buffer.get(bytes);
if(LYRICSBEGIN.equals(StringUtils.getASCIIString(bytes))) {
LOG.debug("found LYRICS3 header tag");
// tag size is the start of the LYRICS3 tag within the buffer to the
// offset location of the last calculated tag
return buffer.limit() - i;
}
}
// start tag was not found
throw new IOException("Could not find BEGIN LYRICS3v1 tag");
}
/**
* Attempts to locate the beginning on a LYRICS3v2.0 tag. This assumes that
* a LYRICS3v2.0 footer tag has already been located. To locate the
* beginning of a LYRICS3v2.0 tag, the 6 bytes preceding the footer tag
* should be read and converted to a number. This number will tell the
* length of the LYRICS3v2.0 tag NOT including the size of the footer tag or
* the 6 bytes describing the Tag size.
*
* After locating the beginning of the LYRICS3 tag, the first 9 bytes must
* read the LYRICS3 starting String "LYRICSBEGIN".
*
* http://www.id3.org/Lyrics3v2
*/
private static long getSizeLyricsTagV2(ByteBuffer buffer, int offset, MP3File mp3File) throws NumberFormatException, IOException {
buffer.position(buffer.limit() - 9 - 6);
byte[] bytes = new byte[6];
buffer.get(bytes);
long startOfTag = Long.parseLong(StringUtils.getASCIIString(bytes));
//sanity check on the tag size
if(startOfTag <= 0 || startOfTag + 9 + 6 + offset > mp3File.getFile().length())
throw new IOException("LYRICS3v2 tag size too large");
//create a new buffer and read the bytes where head of tag is located.
ByteBuffer newByteBuffer = ByteBuffer.allocate(11);
fillBuffer(newByteBuffer, mp3File.getFile(), (int)(offset + startOfTag + 6 + 9 - newByteBuffer.capacity()));
String beginTag = StringUtils.getASCIIString(newByteBuffer.array());
if(LYRICSBEGIN.equals(beginTag)) {
LOG.debug("found LYRICS3v2 header tag");
// return the stated size of the tag plus the 6 bytes describing the size
// plus the 9 bytes of the footer tag
return 9 + 6 + startOfTag;
} else {
//this should never happen, LRYIC BEGIN/END tags should
//always be matching.
throw new IOException("Could not locate BEGIN LYRICS3v2 tag");
}
}
/**
* Returns true if an APE tag is located. APE tags are located at the
* end of a file. APEv1 and APEv2 contain a 32 byte footer. The APE footer
* tag will precede an ID3v1.x tag or be located at the end of the file.
*
* The first 8 bytes of an APE tag footer will contain the String "APETAGEX".
*
* http://wiki.hydrogenaudio.org/index.php?title=APEv2_specification
*/
private static boolean containsAPETag(ByteBuffer buffer) {
byte[] bytes = new byte[8];
buffer.position(buffer.limit() - 32);
buffer.get(bytes);
String footerTag = StringUtils.getASCIIString(bytes);
return APETAG.equals(footerTag);
}
/**
* The APETAG Footer looks like this:
* 8 bytes - "APETAGEX"
* 4 bytes - version number
* 4 bytes - size of tag, including the footer, but NOT including any header
* 4 bytes - number of items in the tag
* 4 bytes - global flags
* 8 bytes - unused (all 0s)
*
* After determining the length of the APETAG, scan back the length of the tag
* plus 32 bytes. APEv2.0 tags will have a matching 32 byte header not specified
* in the length of the tag, APEv1.0 tags will NOT have a header. Check if a
* APETAG header is located at the start of the tag, if so a APETAGv2.0 tag has been
* found, otherwise we assume a APETAGv1.0 tag is being used if a valid footer is located.
*/
private static long getSizeAPETag(ByteBuffer buffer, int offset, MP3File mp3File) throws IOException {
// read the footer of the tag again
byte[] bytes = new byte[32];
buffer.position(buffer.limit() - 32);
buffer.get(bytes);
// the footer of the tag is 32 bytes long, the first 96 bits contains the
// Preabmle String and the version number. The next 32 bits contains a
// little endian Integer that represents the size of the entire tag.
buffer.order(ByteOrder.LITTLE_ENDIAN);
int tagSize = buffer.getInt(12);
//sanity check on this value
if(tagSize <= 0 || offset + tagSize + 32 > mp3File.getFile().length())
throw new IOException("APE tagsize too large");
// locate the start of the tag in a new buffer and check for the TagHeader
ByteBuffer newByteBuffer = ByteBuffer.allocate(8);
fillBuffer(newByteBuffer, mp3File.getFile(), offset + tagSize + 32 - newByteBuffer.capacity());
String headerTag = StringUtils.getASCIIString(newByteBuffer.array());
LOG.debug("found APE header tag");
// if a tag header is found, v2.0 otherwise assume its v1.0
if(APETAG.equals(headerTag)) {
return tagSize + 32; //APEv2.0 tag
} else {
return tagSize; //APEv1.0
}
}
}