/*
* 21.04.2004 Original verion. davagin@udm.ru.
*-----------------------------------------------------------------------
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*----------------------------------------------------------------------
*/
package davaguine.jmac.info;
import davaguine.jmac.tools.*;
import java.io.EOFException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
/**
* Author: Dmitry Vaguine
* Date: 04.03.2004
* Time: 14:51:31
*/
public class APETag implements Comparator {
public final static String APE_TAG_FIELD_TITLE = "Title";
public final static String APE_TAG_FIELD_ARTIST = "Artist";
public final static String APE_TAG_FIELD_ALBUM = "Album";
public final static String APE_TAG_FIELD_COMMENT = "Comment";
public final static String APE_TAG_FIELD_YEAR = "Year";
public final static String APE_TAG_FIELD_TRACK = "Track";
public final static String APE_TAG_FIELD_GENRE = "Genre";
public final static String APE_TAG_FIELD_COVER_ART_FRONT = "Cover Art (front)";
public final static String APE_TAG_FIELD_NOTES = "Notes";
public final static String APE_TAG_FIELD_LYRICS = "Lyrics";
public final static String APE_TAG_FIELD_COPYRIGHT = "Copyright";
public final static String APE_TAG_FIELD_BUY_URL = "Buy URL";
public final static String APE_TAG_FIELD_ARTIST_URL = "Artist URL";
public final static String APE_TAG_FIELD_PUBLISHER_URL = "Publisher URL";
public final static String APE_TAG_FIELD_FILE_URL = "File URL";
public final static String APE_TAG_FIELD_COPYRIGHT_URL = "Copyright URL";
public final static String APE_TAG_FIELD_MJ_METADATA = "Media Jukebox Metadata";
public final static String APE_TAG_FIELD_TOOL_NAME = "Tool Name";
public final static String APE_TAG_FIELD_TOOL_VERSION = "Tool Version";
public final static String APE_TAG_FIELD_PEAK_LEVEL = "Peak Level";
public final static String APE_TAG_FIELD_REPLAY_GAIN_RADIO = "Replay Gain (radio)";
public final static String APE_TAG_FIELD_REPLAY_GAIN_ALBUM = "Replay Gain (album)";
public final static String APE_TAG_FIELD_COMPOSER = "Composer";
public final static String APE_TAG_FIELD_KEYWORDS = "Keywords";
/**
* **************************************************************************************
* Footer (and header) flags
* ***************************************************************************************
*/
public final static int APE_TAG_FLAG_CONTAINS_HEADER = (1 << 31);
public final static int APE_TAG_FLAG_CONTAINS_FOOTER = (1 << 30);
public final static int APE_TAG_FLAG_IS_HEADER = (1 << 29);
public final static int APE_TAG_FLAGS_DEFAULT = APE_TAG_FLAG_CONTAINS_FOOTER;
public final static String APE_TAG_GENRE_UNDEFINED = "Undefined";
// create an APE tag
// bAnalyze determines whether it will analyze immediately or on the first request
// be careful with multiple threads / file pointer movement if you don't analyze immediately
public APETag(File pIO) throws IOException {
this(pIO, true);
}
public APETag(File pIO, boolean bAnalyze) throws IOException {
m_spIO = pIO; // we don't own the IO source
if (bAnalyze)
Analyze();
}
public APETag(String pFilename) throws IOException {
this(pFilename, true);
}
public APETag(String pFilename, boolean bAnalyze) throws IOException {
m_spIO = new RandomAccessFile(new java.io.File(pFilename), "r");
if (bAnalyze)
Analyze();
}
// save the tag to the I/O source (bUseOldID3 forces it to save as an ID3v1.1 tag instead of an APE tag)
public void Save() throws IOException {
Save(false);
}
public void Save(boolean bUseOldID3) throws IOException {
Remove(false);
if (m_aryFields.size() <= 0)
return;
if (!bUseOldID3) {
int z = 0;
// calculate the size of the whole tag
int nFieldBytes = 0;
for (z = 0; z < m_aryFields.size(); z++)
nFieldBytes += ((APETagField) m_aryFields.get(z)).GetFieldSize();
// sort the fields
SortFields();
// build the footer
APETagFooter APETagFooter = new APETagFooter(m_aryFields.size(), nFieldBytes);
// make a buffer for the tag
int nTotalTagBytes = APETagFooter.GetTotalTagBytes();
// save the fields
ByteArrayWriter writer = new ByteArrayWriter(nTotalTagBytes);
for (z = 0; z < m_aryFields.size(); z++)
((APETagField) m_aryFields.get(z)).SaveField(writer);
// add the footer to the buffer
APETagFooter.write(writer);
// dump the tag to the I/O source
WriteBufferToEndOfIO(writer.getBytes());
} else {
// build the ID3 tag
ID3Tag id3tag = new ID3Tag();
CreateID3Tag(id3tag);
ByteArrayWriter writer = new ByteArrayWriter(ID3Tag.ID3_TAG_BYTES);
id3tag.write(writer);
WriteBufferToEndOfIO(writer.getBytes());
}
}
// removes any tags from the file (bUpdate determines whether is should re-analyze after removing the tag)
public void Remove() throws IOException {
Remove(true);
}
public void Remove(boolean bUpdate) throws IOException {
// variables
long nOriginalPosition = m_spIO.getFilePointer();
boolean bID3Removed = true;
boolean bAPETagRemoved = true;
while (bID3Removed || bAPETagRemoved) {
bID3Removed = false;
bAPETagRemoved = false;
// ID3 tag
ID3Tag id3tag = ID3Tag.read(m_spIO);
if (id3tag != null) {
m_spIO.setLength(m_spIO.length() - ID3Tag.ID3_TAG_BYTES);
bID3Removed = true;
}
// APE Tag
APETagFooter footer = APETagFooter.read(m_spIO, false);
if (footer.GetIsValid(true)) {
m_spIO.setLength(m_spIO.length() - footer.GetTotalTagBytes());
bAPETagRemoved = true;
}
}
m_spIO.seek(nOriginalPosition);
if (bUpdate)
Analyze();
}
public void SetFieldString(String pFieldName, String pFieldValue) throws IOException {
// remove if empty
// if ((pFieldValue == null) || (pFieldValue.length() <= 0))
// RemoveField(pFieldName);
if (pFieldValue == null) {
pFieldValue = "";
}
byte[] fieldValue = pFieldValue.getBytes("UTF-8");
byte[] value = new byte[fieldValue.length];
System.arraycopy(fieldValue, 0, value, 0, fieldValue.length);
SetFieldBinary(pFieldName, value, APETagField.TAG_FIELD_FLAG_DATA_TYPE_TEXT_UTF8);
}
public void SetFieldBinary(String pFieldName, byte[] pFieldValue, int nFieldFlags) throws IOException {
if (!m_bAnalyzed)
Analyze();
if (pFieldName == null)
return;
// check to see if we're trying to remove the field (by setting it to NULL or an empty string)
boolean bRemoving = (pFieldValue == null) || (pFieldValue.length <= 0);
// get the index
int nFieldIndex = GetTagFieldIndex(pFieldName);
if (nFieldIndex >= 0) {
// existing field
// fail if we're read-only (and not ignoring the read-only flag)
if ((!m_bIgnoreReadOnly) && ((APETagField) m_aryFields.get(nFieldIndex)).GetIsReadOnly())
return;
// erase the existing field
if (bRemoving)
RemoveField(nFieldIndex);
m_aryFields.set(nFieldIndex, new APETagField(pFieldName, pFieldValue, nFieldFlags));
} else {
if (bRemoving)
return;
m_aryFields.add(new APETagField(pFieldName, pFieldValue, nFieldFlags));
}
}
// gets the value of a field (returns -1 and an empty buffer if the field doesn't exist)
public byte[] GetFieldBinary(String pFieldName) throws IOException {
if (!m_bAnalyzed)
Analyze();
APETagField pAPETagField = GetTagField(pFieldName);
if (pAPETagField == null)
return null;
else
return pAPETagField.GetFieldValue();
}
public String GetFieldString(String pFieldName) throws IOException {
if (!m_bAnalyzed)
Analyze();
String ret = null;
APETagField pAPETagField = GetTagField(pFieldName);
if (pAPETagField != null) {
byte[] b = pAPETagField.GetFieldValue();
int boundary = 0;
int index = b.length - 1;
while (index >= 0 && b[index] == 0) {
index--;
boundary--;
}
if (index < 0)
ret = "";
else {
if (pAPETagField.GetIsUTF8Text() || (m_nAPETagVersion < 2000)) {
// if (m_nAPETagVersion >= 2000)
ret = new String(b, 0, b.length + boundary, "UTF-8");
// else
// ret = new String(b, 0, b.length + boundary);
} else
ret = new String(b, 0, b.length + boundary, "UTF-16");
}
}
return ret;
}
// remove a specific field
public void RemoveField(String pFieldName) throws IOException {
RemoveField(GetTagFieldIndex(pFieldName));
}
public void RemoveField(int nIndex) {
m_aryFields.remove(nIndex);
}
// clear all the fields
public void ClearFields() {
m_aryFields.clear();
}
// get the total tag bytes in the file from the last analyze
// need to call Save() then Analyze() to update any changes
public int GetTagBytes() throws IOException {
if (!m_bAnalyzed)
Analyze();
return m_nTagBytes;
}
// see whether the file has an ID3 or APE tag
public boolean GetHasID3Tag() throws IOException {
if (!m_bAnalyzed)
Analyze();
return m_bHasID3Tag;
}
public boolean GetHasAPETag() throws IOException {
if (!m_bAnalyzed)
Analyze();
return m_bHasAPETag;
}
public int GetAPETagVersion() throws IOException {
return GetHasAPETag() ? m_nAPETagVersion : -1;
}
// gets a desired tag field (returns NULL if not found)
// again, be careful, because this a pointer to the actual field in this class
public APETagField GetTagField(String pFieldName) throws IOException {
int nIndex = GetTagFieldIndex(pFieldName);
return (nIndex != -1) ? (APETagField) m_aryFields.get(nIndex) : null;
}
public APETagField GetTagField(int nIndex) throws IOException {
if (!m_bAnalyzed)
Analyze();
if ((nIndex >= 0) && (nIndex < m_aryFields.size()))
return (APETagField) m_aryFields.get(nIndex);
return null;
}
public void SetIgnoreReadOnly(boolean bIgnoreReadOnly) {
m_bIgnoreReadOnly = bIgnoreReadOnly;
}
// fills in an ID3_TAG using the current fields (useful for quickly converting the tag)
public void CreateID3Tag(ID3Tag pID3Tag) throws IOException {
if (pID3Tag == null)
return;
if (!m_bAnalyzed)
Analyze();
if (m_aryFields.size() <= 0)
return;
pID3Tag.Header = "TAG";
pID3Tag.Artist = GetFieldID3String(APE_TAG_FIELD_ARTIST);
pID3Tag.Album = GetFieldID3String(APE_TAG_FIELD_ALBUM);
pID3Tag.Title = GetFieldID3String(APE_TAG_FIELD_TITLE);
pID3Tag.Comment = GetFieldID3String(APE_TAG_FIELD_COMMENT);
pID3Tag.Year = GetFieldID3String(APE_TAG_FIELD_YEAR);
String track = GetFieldString(APE_TAG_FIELD_TRACK);
try {
pID3Tag.Track = Short.parseShort(track);
} catch (Exception e) {
pID3Tag.Track = 255;
}
pID3Tag.Genre = (short) (new ID3Genre(GetFieldString(APE_TAG_FIELD_GENRE)).getGenre());
}
// private functions
private void Analyze() throws IOException {
// clean-up
ClearFields();
m_nTagBytes = 0;
m_bAnalyzed = true;
// store the original location
long nOriginalPosition = m_spIO.getFilePointer();
// check for a tag
m_bHasID3Tag = false;
m_bHasAPETag = false;
m_nAPETagVersion = -1;
final ID3Tag tag = ID3Tag.read(m_spIO);
if (tag != null) {
m_bHasID3Tag = true;
m_nTagBytes += ID3Tag.ID3_TAG_BYTES;
}
// set the fields
if (m_bHasID3Tag) {
SetFieldID3String(APE_TAG_FIELD_ARTIST, tag.Artist);
SetFieldID3String(APE_TAG_FIELD_ALBUM, tag.Album);
SetFieldID3String(APE_TAG_FIELD_TITLE, tag.Title);
SetFieldID3String(APE_TAG_FIELD_COMMENT, tag.Comment);
SetFieldID3String(APE_TAG_FIELD_YEAR, tag.Year);
SetFieldString(APE_TAG_FIELD_TRACK, String.valueOf(tag.Track));
if ((tag.Genre == ID3Genre.GENRE_UNDEFINED) || (tag.Genre >= ID3Genre.genreCount())) {
// SetFieldString(APE_TAG_FIELD_GENRE, APE_TAG_GENRE_UNDEFINED);
} else
SetFieldString(APE_TAG_FIELD_GENRE, ID3Genre.genreString(tag.Genre));
}
m_spIO.seek(nOriginalPosition);
// try loading the APE tag
m_footer = APETagFooter.read(m_spIO, m_bHasID3Tag);
if (m_footer != null && m_footer.GetIsValid(false)) {
m_bHasAPETag = true;
m_nAPETagVersion = m_footer.GetVersion();
int nRawFieldBytes = m_footer.GetFieldBytes();
m_nTagBytes += m_footer.GetTotalTagBytes();
long pos = m_spIO.length() - m_footer.GetTotalTagBytes() - m_footer.GetFieldsOffset();
if (m_bHasID3Tag)
pos -= ID3Tag.ID3_TAG_BYTES;
m_spIO.seek(pos);
try {
final ByteArrayReader reader = new ByteArrayReader(m_spIO, nRawFieldBytes);
// parse out the raw fields
for (int z = 0; z < m_footer.GetNumberFields(); z++)
LoadField(reader);
} catch (EOFException e) {
throw new JMACException("Can't Read APE Tag Fields");
}
}
// restore the file pointer
m_spIO.seek(nOriginalPosition);
}
private int GetTagFieldIndex(String pFieldName) throws IOException {
if (!m_bAnalyzed)
Analyze();
if (pFieldName == null) return -1;
for (int z = 0; z < m_aryFields.size(); z++) {
if (pFieldName.toLowerCase().equals(((APETagField) m_aryFields.get(z)).GetFieldName().toLowerCase()))
return z;
}
return -1;
}
private void WriteBufferToEndOfIO(byte[] pBuffer) throws IOException {
long nOriginalPosition = m_spIO.getFilePointer();
m_spIO.seek(m_spIO.length());
m_spIO.write(pBuffer);
m_spIO.seek(nOriginalPosition);
}
private void LoadField(ByteArrayReader reader) throws IOException {
// size and flags
int nFieldValueSize = reader.readInt();
int nFieldFlags = reader.readInt();
String fieldName = reader.readString("UTF-8");
// value
byte[] fieldValue = new byte[nFieldValueSize];
reader.readFully(fieldValue);
// set
SetFieldBinary(fieldName, fieldValue, nFieldFlags);
}
private void SortFields() {
// sort the tag fields by size (so that the smallest fields are at the front of the tag)
Arrays.sort(m_aryFields.toArray(), this);
}
public int compare(Object pA, Object pB) {
APETagField pFieldA = (APETagField) pA;
APETagField pFieldB = (APETagField) pB;
return pFieldA.GetFieldSize() - pFieldB.GetFieldSize();
}
// helper set / get field functions
private String GetFieldID3String(String pFieldName) throws IOException {
return GetFieldString(pFieldName);
}
private void SetFieldID3String(String pFieldName, String pFieldValue) throws IOException {
SetFieldString(pFieldName, pFieldValue.trim());
}
public APETagFooter getFooter() {
return m_footer;
}
// private data
private File m_spIO;
private boolean m_bAnalyzed = false;
private int m_nTagBytes = 0;
private List m_aryFields = new ArrayList();
private boolean m_bHasAPETag;
private int m_nAPETagVersion;
private boolean m_bHasID3Tag;
private boolean m_bIgnoreReadOnly = false;
private APETagFooter m_footer = null;
}