/* * 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); } } }