/*
* MP3File.java
*
* Created on 7-Oct-2003
*
* Copyright (C)2003-2005 Paul Grebenc
*
* 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, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* $Id: MP3File.java,v 1.20 2005/11/22 02:09:14 paul Exp $
*/
package org.blinkenlights.jid3;
import java.io.*;
import java.util.*;
import org.blinkenlights.jid3.io.*;
import org.blinkenlights.jid3.v1.*;
import org.blinkenlights.jid3.v2.*;
/**
* @author paul
*
* A class representing an MP3 file.
*/
public class MP3File extends MediaFile
{
/** Construct an object representing the MP3 file specified.
*
* @param oSourceFile a File pointing to the source MP3 file
*/
public MP3File(File oSourceFile)
{
super(oSourceFile);
}
public MP3File(IFileSource oFileSource)
{
super(oFileSource);
}
/* (non-Javadoc)
* @see org.blinkenlights.id3.MediaFile#sync()
*/
public void sync()
throws ID3Exception
{
// before we write anything, check first that if there is a v2 tag to be sync'ed, it is a valid tag to write
// (it is not valid unless it has at least one frame)
if ((m_oID3V2Tag != null) && ( ! m_oID3V2Tag.containsAtLeastOneFrame()))
{
throw new ID3Exception("This file has an ID3 V2 tag which cannot be written because it does not contain at least one frame.");
}
if (m_oID3V1Tag != null)
{
// need to update the V1 tags
v1Sync();
}
if (m_oID3V2Tag != null)
{
// need to update the V2 tags
v2Sync();
}
}
/** Update the contents of the actual MP3 file to reflect the current ID3 V1 tag settings of the object.
*
* @throws ID3Exception if an error updating the file occurs
*/
private void v1Sync()
throws ID3Exception
{
IFileSource oTmpFileSource = null;
InputStream oSourceIS = null;
OutputStream oTmpOS = null;
try
{
// open source file for reading
try
{
oSourceIS = new BufferedInputStream(m_oFileSource.getInputStream());
}
catch (Exception e)
{
throw new ID3Exception("Error opening [" + m_oFileSource.getName() + "]", e);
}
try
{
// create temporary file to work with
try
{
oTmpFileSource = m_oFileSource.createTempFile("id3.", ".tmp");
}
catch (Exception e)
{
throw new ID3Exception("Unable to create temporary file.", e);
}
// open temp file for writing
try
{
oTmpOS = oTmpFileSource.getOutputStream();
}
catch (Exception e)
{
throw new ID3Exception("Error opening temporary file for writing.", e);
}
try
{
// copy over all of the source file up to but not including the V1 tags,
// if they are present
long lFileLength = m_oFileSource.length();
// copy over all of the file up to the last 128 bytes (in 64k blocks for speed, while remaining memory efficient)
byte[] abyBuffer = new byte[65536];
long lCopied = 0;
long lTotalToCopy = lFileLength - 128;
while (lCopied < lTotalToCopy)
{
long lLeftToCopy = lTotalToCopy - lCopied;
long lToCopyNow = (lLeftToCopy >= 65536) ? 65536 : lLeftToCopy;
oSourceIS.read(abyBuffer, 0, (int)lToCopyNow);
oTmpOS.write(abyBuffer, 0, (int)lToCopyNow);
lCopied += lToCopyNow;
}
// check next three bytes of source file which indicate whether this file already
// has a V1 tag on it or not
byte[] abyCheckTag = new byte[3];
oSourceIS.read(abyCheckTag);
if ( ! ((abyCheckTag[0] == 'T') && (abyCheckTag[1] == 'A') && (abyCheckTag[2] == 'G')))
{
// no V1 tag on this file... copy the rest of it over (3 + 125 = 128 bytes)
oTmpOS.write(abyCheckTag);
for (int i=0; i < 125; i++)
{
oTmpOS.write(oSourceIS.read());
}
}
// append V1 tag information to the end of the data copied from the source file
// to the temporary file
m_oID3V1Tag.write(oTmpOS);
// we're done
oTmpOS.flush();
}
finally
{
oTmpOS.close();
}
}
finally
{
oSourceIS.close();
}
// move temp file to original source file
if (! m_oFileSource.delete())
{
//HACK: This is a hack, to get around the fact that at least some JVMs are buggy, in that files which
// have been closed are hung onto, pending garbage collection. By suggesting garbage collection,
// the next time, the delete -magically- works.
int iFails = 1;
int iDelay = 1;
while (!m_oFileSource.delete())
{
System.gc(); // this will close the open file
Thread.sleep(iDelay);
iFails++;
iDelay *= 2;
if (iFails > 10)
{
throw new ID3Exception("Unable to delete original file.");
}
}
}
if (! oTmpFileSource.renameTo(m_oFileSource))
{
throw new ID3Exception("Unable to rename temporary file " + oTmpFileSource.toString() + " to " + m_oFileSource.toString() + ".");
}
}
catch (ID3Exception e)
{
throw e;
}
catch (Exception e)
{
throw new ID3Exception("Error processing [" + m_oFileSource.getName() + "].", e);
}
}
/** Update the contents of the actual MP3 file to reflect the current ID3 V2 tag settings of the object.
*
* @throws ID3Exception if an error updating the file occurs
*/
private void v2Sync()
throws ID3Exception
{
IFileSource oTmpFileSource = null;
InputStream oSourceIS = null;
OutputStream oTmpOS = null;
// check first if this tag can be written (ie. unregistered crypto agents, etc.)
m_oID3V2Tag.sanityCheck();
try
{
// open source file for reading
try
{
oSourceIS = new BufferedInputStream(m_oFileSource.getInputStream());
}
catch (Exception e)
{
throw new ID3Exception("Error opening [" + m_oFileSource.getName() + "]", e);
}
try
{
// create temporary file to work with
try
{
oTmpFileSource = m_oFileSource.createTempFile("id3.", ".tmp");
}
catch (Exception e)
{
throw new ID3Exception("Unable to create temporary file.", e);
}
// open temp file for writing
try
{
oTmpOS = new BufferedOutputStream(oTmpFileSource.getOutputStream());
}
catch (Exception e)
{
throw new ID3Exception("Error opening temporary file for writing.", e);
}
try
{
// write tag to beginning of file (if we were going to write somewhere other than the beginning,
// we'd have to deal with that somewhere in this method)
m_oID3V2Tag.write(oTmpOS);
// copy over the MP3 data from the source file to the temp file
// (need to check if there is a V2 tag in the source file, and skip over them if they exist)
byte[] abyCheckTag = new byte[3];
oSourceIS.read(abyCheckTag);
if ((abyCheckTag[0] == 'I') && (abyCheckTag[1] == 'D') && (abyCheckTag[2] == '3'))
{
// there is a tag in this file.. skip over them
// read version information
int iVersion = oSourceIS.read();
int iPatch = oSourceIS.read();
if (iVersion > 4)
{
// close and remove temp file
oTmpOS.close();
oTmpFileSource.delete();
throw new ID3Exception("Will not overwrite tag of version greater than 2.4.0.");
}
// skip flags
oSourceIS.skip(1);
// get tag length
byte[] abyTagLength = new byte[4];
if (oSourceIS.read(abyTagLength) != 4)
{
throw new ID3Exception("Error reading existing ID3 tag.");
}
ID3DataInputStream oID3DIS = new ID3DataInputStream(new ByteArrayInputStream(abyTagLength));
long iTagLength = oID3DIS.readID3Four();
oID3DIS.close();
while (iTagLength > 0)
{
long iNumSkipped = oSourceIS.skip(iTagLength);
if (iNumSkipped == 0)
{
throw new ID3Exception("Error reading existing ID3 tag.");
}
iTagLength -= iNumSkipped;
}
}
else
{
// there is no tag in this file...
oTmpOS.write(abyCheckTag);
}
// copy over rest of the file
byte[] abyBuffer = new byte[65536];
int iNumRead;
while ((iNumRead = oSourceIS.read(abyBuffer)) != -1)
{
oTmpOS.write(abyBuffer, 0, iNumRead);
}
// we're done
oTmpOS.flush();
}
finally
{
oTmpOS.close();
}
}
finally
{
oSourceIS.close();
}
// move temp file to original source file
if (! m_oFileSource.delete())
{
//HACK: This is a hack, to get around the fact that at least some JVMs are buggy, in that files which
// have been closed are hung onto, pending garbage collection. By suggesting garbage collection,
// the next time, the delete -magically- works.
int iFails = 1;
int iDelay = 1;
while (!m_oFileSource.delete())
{
System.gc(); // this will close the open file
Thread.sleep(iDelay);
iFails++;
iDelay *= 2;
if (iFails > 10)
{
throw new ID3Exception("Unable to delete original file.");
}
}
}
if (! oTmpFileSource.renameTo(m_oFileSource))
{
throw new ID3Exception("Unable to rename temporary file " + oTmpFileSource.toString() + " to " + m_oFileSource.toString() + ".");
}
}
catch (ID3Exception e)
{
throw e;
}
catch (Exception e)
{
throw new ID3Exception("Error processing [" + m_oFileSource.getName() + "].", e);
}
}
/* (non-Javadoc)
* @see org.blinkenlights.id3.MediaFile#getTags()
*/
public ID3Tag[] getTags()
throws ID3Exception
{
List oID3TagList = new ArrayList();
// get ID3V1Tag if they exist
ID3V1Tag oID3V1Tag = getID3V1Tag();
if (oID3V1Tag != null)
{
oID3TagList.add(oID3V1Tag);
}
// get ID3V2Tag if they exist
ID3V2Tag oID3V2Tag = getID3V2Tag();
if (oID3V2Tag != null)
{
oID3TagList.add(oID3V2Tag);
}
return (ID3Tag[])oID3TagList.toArray(new ID3Tag[0]);
}
public ID3V1Tag getID3V1Tag()
throws ID3Exception
{
try
{
InputStream oSourceIS = new BufferedInputStream(m_oFileSource.getInputStream());
try
{
// copy over all of the file up to the last 128 bytes
long lFileLength = m_oFileSource.length();
oSourceIS.skip(lFileLength - 128);
// check if V1 tag is present
byte[] abyCheckTag = new byte[3];
oSourceIS.read(abyCheckTag);
if ((abyCheckTag[0] == 'T') && (abyCheckTag[1] == 'A') && (abyCheckTag[2] == 'G'))
{
// there is a tag, we must read it
ID3V1Tag oID3V1Tag = ID3V1Tag.read(oSourceIS);
return oID3V1Tag;
}
else
{
return null;
}
}
finally
{
oSourceIS.close();
}
}
catch (Exception e)
{
throw new ID3Exception(e);
}
}
public ID3V2Tag getID3V2Tag()
throws ID3Exception
{
//TODO: We're only checking for v2.3.0 tags here now. We'd otherwise have to find
// the "ID3" identifier in the file first.
try
{
InputStream oSourceIS = new BufferedInputStream(m_oFileSource.getInputStream());
ID3DataInputStream oSourceID3DIS = new ID3DataInputStream(oSourceIS);
try
{
// check if v2 tag is present
byte[] abyCheckTag = new byte[3];
oSourceID3DIS.readFully(abyCheckTag);
if ((abyCheckTag[0] == 'I') && (abyCheckTag[1] == 'D') && (abyCheckTag[2] == '3'))
{
return ID3V2Tag.read(oSourceID3DIS);
}
else
{
return null;
}
}
finally
{
oSourceID3DIS.close();
}
}
catch (ID3Exception e)
{
throw e;
}
catch (Exception e)
{
throw new ID3Exception("Error reading tags from file.", e);
}
}
public void removeTags()
throws ID3Exception
{
removeID3V1Tag();
removeID3V2Tag();
}
public void removeID3V1Tag()
throws ID3Exception
{
IFileSource oTmpFileSource = null;
InputStream oSourceIS = null;
OutputStream oTmpOS = null;
try
{
// open source file for reading
try
{
oSourceIS = new BufferedInputStream(m_oFileSource.getInputStream());
}
catch (Exception e)
{
throw new ID3Exception("Error opening [" + m_oFileSource.getName() + "]", e);
}
try
{
// create temporary file to work with
try
{
oTmpFileSource = m_oFileSource.createTempFile("id3.", ".tmp");
}
catch (Exception e)
{
throw new ID3Exception("Unable to create temporary file.", e);
}
// open temp file for writing
try
{
oTmpOS = new BufferedOutputStream(oTmpFileSource.getOutputStream());
}
catch (Exception e)
{
throw new ID3Exception("Error opening temporary file for writing.", e);
}
try
{
// copy over all of the source file up to but not including the V1 tags,
// if they are present
long lFileLength = m_oFileSource.length();
// copy over all of the file up to the last 128 bytes (in 64k blocks for speed, while remaining memory efficient)
byte[] abyBuffer = new byte[65536];
long lCopied = 0;
long lTotalToCopy = lFileLength - 128;
while (lCopied < lTotalToCopy)
{
long lLeftToCopy = lTotalToCopy - lCopied;
long lToCopyNow = (lLeftToCopy >= 65536) ? 65536 : lLeftToCopy;
oSourceIS.read(abyBuffer, 0, (int)lToCopyNow);
oTmpOS.write(abyBuffer, 0, (int)lToCopyNow);
lCopied += lToCopyNow;
}
// check next three bytes of source file which indicate whether this file already
// has a V1 tag on it or not
byte[] abyCheckTag = new byte[3];
oSourceIS.read(abyCheckTag);
if ( ! ((abyCheckTag[0] == 'T') && (abyCheckTag[1] == 'A') && (abyCheckTag[2] == 'G')))
{
// no V1 tag on this file... copy the rest of it over (3 + 125 = 128 bytes)
oTmpOS.write(abyCheckTag);
for (int i=0; i < 125; i++)
{
oTmpOS.write(oSourceIS.read());
}
}
// we're done
oTmpOS.flush();
}
finally
{
oTmpOS.close();
}
}
finally
{
oSourceIS.close();
}
// move temp file to original source file
if (! m_oFileSource.delete())
{
//HACK: This is a hack, to get around the fact that at least some JVMs are buggy, in that files which
// have been closed are hung onto, pending garbage collection. By suggesting garbage collection,
// the next time, the delete -magically- works.
int iFails = 1;
int iDelay = 1;
while (!m_oFileSource.delete())
{
System.gc(); // this will close the open file
Thread.sleep(iDelay);
iFails++;
iDelay *= 2;
if (iFails > 10)
{
throw new ID3Exception("Unable to delete original file.");
}
}
}
if (! oTmpFileSource.renameTo(m_oFileSource))
{
throw new ID3Exception("Unable to rename temporary file " + oTmpFileSource.toString() + " to " + m_oFileSource.toString() + ".");
}
}
catch (ID3Exception e)
{
throw e;
}
catch (Exception e)
{
throw new ID3Exception("Error processing [" + m_oFileSource.getName() + "].", e);
}
}
public void removeID3V2Tag()
throws ID3Exception
{
IFileSource oTmpFileSource = null;
InputStream oSourceIS = null;
OutputStream oTmpOS = null;
// create temporary file to work with
try
{
oTmpFileSource = m_oFileSource.createTempFile("id3.", ".tmp");
}
catch (Exception e)
{
throw new ID3Exception("Unable to create temporary file.", e);
}
try
{
// open source file for reading
try
{
oSourceIS = new BufferedInputStream(m_oFileSource.getInputStream());
}
catch (Exception e)
{
throw new ID3Exception("Error opening [" + m_oFileSource.getName() + "]", e);
}
try
{
// open temp file for writing
try
{
oTmpOS = new BufferedOutputStream(oTmpFileSource.getOutputStream());
}
catch (Exception e)
{
throw new ID3Exception("Error opening temporary file for writing.", e);
}
try
{
// copy over the MP3 data from the source file to the temp file
// (need to check if there is a V2 tag in the source file, and skip over them if they exist)
byte[] abyCheckTag = new byte[3];
oSourceIS.read(abyCheckTag);
if ((abyCheckTag[0] == 'I') && (abyCheckTag[1] == 'D') && (abyCheckTag[2] == '3'))
{
// there is a tag in this file.. skip over it
// read version information
int iVersion = oSourceIS.read();
int iPatch = oSourceIS.read();
// skip flags
oSourceIS.skip(1);
// get tag length
byte[] abyTagLength = new byte[4];
if (oSourceIS.read(abyTagLength) != 4)
{
throw new ID3Exception("Error reading existing ID3 tags.");
}
ID3DataInputStream oID3DIS = new ID3DataInputStream(new ByteArrayInputStream(abyTagLength));
long iTagLength = oID3DIS.readID3Four();
oID3DIS.close();
while (iTagLength > 0)
{
long iNumSkipped = oSourceIS.skip(iTagLength);
if (iNumSkipped == 0)
{
throw new ID3Exception("Error reading existing ID3 tag.");
}
iTagLength -= iNumSkipped;
}
}
else
{
// there are no tags in this file...
oTmpOS.write(abyCheckTag);
}
// copy over rest of the file
byte[] abyBuffer = new byte[65536];
int iNumRead;
while ((iNumRead = oSourceIS.read(abyBuffer)) != -1)
{
oTmpOS.write(abyBuffer, 0, iNumRead);
}
// we're done
oTmpOS.flush();
}
finally
{
oTmpOS.close();
}
}
finally
{
oSourceIS.close();
}
// move temp file to original source file
if (! m_oFileSource.delete())
{
//HACK: This is a hack, to get around the fact that at least some JVMs are buggy, in that files which
// have been closed are hung onto, pending garbage collection. By suggesting garbage collection,
// the next time, the delete -magically- works.
int iFails = 1;
int iDelay = 1;
while (!m_oFileSource.delete())
{
System.gc(); // this will close the open file
Thread.sleep(iDelay);
iFails++;
iDelay *= 2;
if (iFails > 10)
{
throw new ID3Exception("Unable to delete original file.");
}
}
}
if (! oTmpFileSource.renameTo(m_oFileSource))
{
throw new ID3Exception("Unable to rename temporary file " + oTmpFileSource.toString() + " to " + m_oFileSource.toString() + ".");
}
}
catch (ID3Exception e)
{
throw e;
}
catch (Exception e)
{
throw new ID3Exception("Error processing [" + m_oFileSource.getName() + "].", e);
}
}
}