/* * MusicTag Copyright (C)2003,2004 * * 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, * you can get a copy from http://www.opensource.org/licenses/lgpl-license.php or write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.jaudiotagger.tag.id3; import org.jaudiotagger.audio.generic.Utils; import org.jaudiotagger.audio.mp3.MP3File; import org.jaudiotagger.tag.*; import org.jaudiotagger.tag.id3.framebody.AbstractID3v2FrameBody; import org.jaudiotagger.tag.id3.framebody.FrameBodyDeprecated; import org.jaudiotagger.tag.id3.framebody.FrameBodyUnsupported; import org.jaudiotagger.tag.id3.valuepair.TextEncoding; import org.jaudiotagger.utils.EqualsUtil; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigInteger; import java.nio.ByteBuffer; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Represents an ID3v2.2 frame. * * @author : Paul Taylor * @author : Eric Farng * @version $Id: ID3v22Frame.java 925 2010-10-21 15:22:41Z paultaylor $ */ public class ID3v22Frame extends AbstractID3v2Frame { private static Pattern validFrameIdentifier = Pattern.compile("[A-Z][0-9A-Z]{2}"); protected static final int FRAME_ID_SIZE = 3; protected static final int FRAME_SIZE_SIZE = 3; protected static final int FRAME_HEADER_SIZE = FRAME_ID_SIZE + FRAME_SIZE_SIZE; public ID3v22Frame() { } protected int getFrameIdSize() { return FRAME_ID_SIZE; } protected int getFrameSizeSize() { return FRAME_SIZE_SIZE; } protected int getFrameHeaderSize() { return FRAME_HEADER_SIZE; } /** * Creates a new ID3v22 Frame with given body * * @param body New body and frame is based on this */ public ID3v22Frame(AbstractID3v2FrameBody body) { super(body); } /** * Compare for equality * To be deemed equal obj must be a IDv23Frame with the same identifier * and the same flags. * containing the same body,datatype list ectera. * equals() method is made up from all the various components * * @param obj * @return if true if this object is equivalent to obj */ public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof ID3v22Frame)) { return false; } ID3v22Frame that = (ID3v22Frame) obj; return EqualsUtil.areEqual(this.statusFlags, that.statusFlags) && EqualsUtil.areEqual(this.encodingFlags, that.encodingFlags) && super.equals(that); } /** * Creates a new ID3v22 Frame of type identifier. * <p/> * An empty body of the correct type will be automatically created. This constructor should be used when wish to * create a new frame from scratch using user values * * @param identifier */ @SuppressWarnings("unchecked") public ID3v22Frame(String identifier) { //logger.info("Creating empty frame of type" + identifier); String bodyIdentifier = identifier; this.identifier = identifier; //If dealing with v22 identifier (Note this constructor is used by all three tag versions) if (ID3Tags.isID3v22FrameIdentifier(bodyIdentifier)) { //Does it have its own framebody (PIC,CRM) or are we using v23/v24 body (the normal case) if (ID3Tags.forceFrameID22To23(bodyIdentifier) != null) { //Do not convert } else if (bodyIdentifier.equals("CRM")) { //Do not convert. //TODO we don't have a way of converting this to v23 which is why its not in the ForceMap } //TODO Improve messy fix for datetime //TODO need to check in case v22 body does exist before using V23 body(e.g PIC) else if ((bodyIdentifier.equals(ID3v22Frames.FRAME_ID_V2_TYER)) || (bodyIdentifier.equals(ID3v22Frames.FRAME_ID_V2_TIME))) { bodyIdentifier = ID3v24Frames.FRAME_ID_YEAR; } // Have to check for v22 because most don't have own body they use v23 or v24 // body to hold the data, the frame is identified by its identifier, the body identifier // is just to create a body suitable for writing the data to else if (ID3Tags.isID3v22FrameIdentifier(bodyIdentifier)) { bodyIdentifier = ID3Tags.convertFrameID22To23(bodyIdentifier); } } // Use reflection to map id to frame body, which makes things much easier // to keep things up to date. try { Class<AbstractID3v2FrameBody> c = (Class<AbstractID3v2FrameBody>) Class.forName("org.jaudiotagger.tag.id3.framebody.FrameBody" + bodyIdentifier); frameBody = c.newInstance(); } catch (ClassNotFoundException cnfe) { logger.log(Level.SEVERE, cnfe.getMessage(), cnfe); frameBody = new FrameBodyUnsupported(identifier); } //Instantiate Interface/Abstract should not happen catch (InstantiationException ie) { logger.log(Level.SEVERE, ie.getMessage(), ie); throw new RuntimeException(ie); } //Private Constructor shouild not happen catch (IllegalAccessException iae) { logger.log(Level.SEVERE, iae.getMessage(), iae); throw new RuntimeException(iae); } frameBody.setHeader(this); //logger.info("Created empty frame of type" + this.identifier + "with frame body of" + bodyIdentifier); } /** * Copy Constructor * <p/> * Creates a new v22 frame based on another v22 frame * * @param frame */ public ID3v22Frame(ID3v22Frame frame) { super(frame); //logger.info("Creating frame from a frame of same version"); } private void createV22FrameFromV23Frame(ID3v23Frame frame) throws InvalidFrameException { identifier = ID3Tags.convertFrameID23To22(frame.getIdentifier()); if (identifier != null) { //logger.info("V2:Orig id is:" + frame.getIdentifier() + ":New id is:" + identifier); this.frameBody = (AbstractID3v2FrameBody) ID3Tags.copyObject(frame.getBody()); } // Is it a known v3 frame which needs forcing to v2 frame e.g. APIC - PIC else if (ID3Tags.isID3v23FrameIdentifier(frame.getIdentifier())) { identifier = ID3Tags.forceFrameID23To22(frame.getIdentifier()); if (identifier != null) { //logger.info("V2:Force:Orig id is:" + frame.getIdentifier() + ":New id is:" + identifier); this.frameBody = this.readBody(identifier, (AbstractID3v2FrameBody) frame.getBody()); } // No mechanism exists to convert it to a v22 frame else { throw new InvalidFrameException("Unable to convert v23 frame:" + frame.getIdentifier() + " to a v22 frame"); } } //Deprecated frame for v23 else if (frame.getBody() instanceof FrameBodyDeprecated) { //Was it valid for this tag version, if so try and reconstruct if (ID3Tags.isID3v22FrameIdentifier(frame.getIdentifier())) { this.frameBody = frame.getBody(); identifier = frame.getIdentifier(); //logger.info("DEPRECATED:Orig id is:" + frame.getIdentifier() + ":New id is:" + identifier); } //or was it still deprecated, if so leave as is else { this.frameBody = new FrameBodyDeprecated((FrameBodyDeprecated) frame.getBody()); identifier = frame.getIdentifier(); //logger.info("DEPRECATED:Orig id is:" + frame.getIdentifier() + ":New id is:" + identifier); } } // Unknown Frame e.g NCON else { this.frameBody = new FrameBodyUnsupported((FrameBodyUnsupported) frame.getBody()); identifier = frame.getIdentifier(); //logger.info("v2:UNKNOWN:Orig id is:" + frame.getIdentifier() + ":New id is:" + identifier); } } /** * Creates a new ID3v22 Frame from another frame of a different tag version * * @param frame to construct the new frame from * @throws org.jaudiotagger.tag.InvalidFrameException * */ public ID3v22Frame(AbstractID3v2Frame frame) throws InvalidFrameException { //logger.info("Creating frame from a frame of a different version"); if (frame instanceof ID3v22Frame) { throw new UnsupportedOperationException("Copy Constructor not called. Please type cast the argument"); } // If it is a v24 frame is it possible to convert it into a v23 frame, anmd then convert from that if (frame instanceof ID3v24Frame) { ID3v23Frame v23Frame = new ID3v23Frame(frame); createV22FrameFromV23Frame(v23Frame); } //If it is a v23 frame is it possible to convert it into a v22 frame else if (frame instanceof ID3v23Frame) { createV22FrameFromV23Frame((ID3v23Frame) frame); } this.frameBody.setHeader(this); //logger.info("Created frame from a frame of a different version"); } /** * Creates a new ID3v22Frame datatype by reading from byteBuffer. * * @param byteBuffer to read from * @param loggingFilename * @throws org.jaudiotagger.tag.InvalidFrameException * */ public ID3v22Frame(ByteBuffer byteBuffer, String loggingFilename) throws InvalidFrameException, InvalidDataTypeException { setLoggingFilename(loggingFilename); read(byteBuffer); } /** * Creates a new ID3v23Frame datatype by reading from byteBuffer. * * @param byteBuffer to read from * @throws org.jaudiotagger.tag.InvalidFrameException * * @deprecated use {@link #ID3v22Frame(ByteBuffer, String)} instead */ public ID3v22Frame(ByteBuffer byteBuffer) throws InvalidFrameException, InvalidDataTypeException { this(byteBuffer, ""); } /** * Return size of frame * * @return int size of frame */ public int getSize() { return frameBody.getSize() + getFrameHeaderSize(); } @Override protected boolean isPadding(byte[] buffer) { if ( (buffer[0] == '\0') && (buffer[1] == '\0') && (buffer[2] == '\0') ) { return true; } return false; } /** * Read frame from file. * Read the frame header then delegate reading of data to frame body. * * @param byteBuffer */ public void read(ByteBuffer byteBuffer) throws InvalidFrameException, InvalidDataTypeException { String identifier = readIdentifier(byteBuffer); byte[] buffer = new byte[getFrameSizeSize()]; // Is this a valid identifier? if (!isValidID3v2FrameIdentifier(identifier)) { //logger.info("Invalid identifier:" + identifier); byteBuffer.position(byteBuffer.position() - (getFrameIdSize() - 1)); throw new InvalidFrameIdentifierException(getLoggingFilename() + ":" + identifier + ":is not a valid ID3v2.20 frame"); } //Read Frame Size (same size as Frame Id so reuse buffer) byteBuffer.get(buffer, 0, getFrameSizeSize()); frameSize = decodeSize(buffer); if (frameSize < 0) { throw new InvalidFrameException(identifier + " has invalid size of:" + frameSize); } else if (frameSize == 0) { //We dont process this frame or add to framemap becuase contains no useful information //logger.warning("Empty Frame:" + identifier); throw new EmptyFrameException(identifier + " is empty frame"); } else if (frameSize > byteBuffer.remaining()) { //logger.warning("Invalid Frame size larger than size before mp3 audio:" + identifier); throw new InvalidFrameException(identifier + " is invalid frame"); } else { //logger.fine("Frame Size Is:" + frameSize); //Convert v2.2 to v2.4 id just for reading the data String id = ID3Tags.convertFrameID22To24(identifier); if (id == null) { //OK,it may be convertable to a v.3 id even though not valid v.4 id = ID3Tags.convertFrameID22To23(identifier); if (id == null) { // Is it a valid v22 identifier so should be able to find a // frame body for it. if (ID3Tags.isID3v22FrameIdentifier(identifier)) { id = identifier; } // Unknown so will be created as FrameBodyUnsupported else { id = UNSUPPORTED_ID; } } } //logger.fine("Identifier was:" + identifier + " reading using:" + id); //Create Buffer that only contains the body of this frame rather than the remainder of tag ByteBuffer frameBodyBuffer = byteBuffer.slice(); frameBodyBuffer.limit(frameSize); try { frameBody = readBody(id, frameBodyBuffer, frameSize); } finally { //Update position of main buffer, so no attempt is made to reread these bytes byteBuffer.position(byteBuffer.position() + frameSize); } } } /** * Read Frame Size, which has to be decoded * * @param buffer * @return */ private int decodeSize(byte[] buffer) { BigInteger bi = new BigInteger(buffer); int tmpSize = bi.intValue(); if (tmpSize < 0) { //logger.warning("Invalid Frame Size of:" + tmpSize + "Decoded from bin:" + Integer.toBinaryString(tmpSize) + "Decoded from hex:" + Integer.toHexString(tmpSize)); } return tmpSize; } /** * Write Frame raw data * * @throws IOException */ public void write(ByteArrayOutputStream tagBuffer) { //logger.info("Write Frame to Buffer" + getIdentifier()); //This is where we will write header, move position to where we can //write body ByteBuffer headerBuffer = ByteBuffer.allocate(getFrameHeaderSize()); //Write Frame Body Data ByteArrayOutputStream bodyOutputStream = new ByteArrayOutputStream(); ((AbstractID3v2FrameBody) frameBody).write(bodyOutputStream); //Write Frame Header //Write Frame ID must adjust can only be 3 bytes long headerBuffer.put(Utils.getDefaultBytes(getIdentifier(), "ISO-8859-1"), 0, getFrameIdSize()); encodeSize(headerBuffer, frameBody.getSize()); //Add header to the Byte Array Output Stream try { tagBuffer.write(headerBuffer.array()); //Add body to the Byte Array Output Stream tagBuffer.write(bodyOutputStream.toByteArray()); } catch (IOException ioe) { //This could never happen coz not writing to file, so convert to RuntimeException throw new RuntimeException(ioe); } } /** * Write Frame Size (can now be accurately calculated, have to convert 4 byte int * to 3 byte format. * * @param headerBuffer * @param size */ private void encodeSize(ByteBuffer headerBuffer, int size) { headerBuffer.put((byte) ((size & 0x00FF0000) >> 16)); headerBuffer.put((byte) ((size & 0x0000FF00) >> 8)); headerBuffer.put((byte) (size & 0x000000FF)); //logger.fine("Frame Size Is Actual:" + size + ":Encoded bin:" + Integer.toBinaryString(size) + ":Encoded Hex" + Integer.toHexString(size)); } /** * Does the frame identifier meet the syntax for a idv3v2 frame identifier. * must start with a capital letter and only contain capital letters and numbers * * @param identifier * @return */ public boolean isValidID3v2FrameIdentifier(String identifier) { Matcher m = ID3v22Frame.validFrameIdentifier.matcher(identifier); return m.matches(); } /** * Return String Representation of body */ public void createStructure() { MP3File.getStructureFormatter().openHeadingElement(TYPE_FRAME, getIdentifier()); MP3File.getStructureFormatter().addElement(TYPE_FRAME_SIZE, frameSize); frameBody.createStructure(); MP3File.getStructureFormatter().closeHeadingElement(TYPE_FRAME); } /** * @return true if considered a common frame */ public boolean isCommon() { return ID3v22Frames.getInstanceOf().isCommon(getId()); } /** * @return true if considered a common frame */ public boolean isBinary() { return ID3v22Frames.getInstanceOf().isBinary(getId()); } /** * Sets the charset encoding used by the field. * * @param encoding charset. */ public void setEncoding(String encoding) { Integer encodingId = TextEncoding.getInstanceOf().getIdForValue(encoding); if (encoding != null) { if (encodingId < 2) { this.getBody().setTextEncoding(encodingId.byteValue()); } } } }