/** * @author : Paul Taylor * @author : Eric Farng * * Version @version:$Id: ID3v11Tag.java 910 2010-08-04 18:50:13Z paultaylor $ * * 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 * * Description: * This class is for a ID3v1.1 Tag * */ package org.jaudiotagger.tag.id3; import org.jaudiotagger.audio.generic.Utils; import org.jaudiotagger.audio.mp3.MP3File; import org.jaudiotagger.logging.ErrorMessage; import org.jaudiotagger.tag.*; import org.jaudiotagger.tag.id3.framebody.*; import org.jaudiotagger.tag.id3.valuepair.TextEncoding; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; /** * Represents an ID3v11 tag. * * @author : Eric Farng * @author : Paul Taylor */ public class ID3v11Tag extends ID3v1Tag { //For writing output protected static final String TYPE_TRACK = "track"; protected static final int TRACK_UNDEFINED = 0; protected static final int TRACK_MAX_VALUE = 255; protected static final int TRACK_MIN_VALUE = 1; protected static final int FIELD_COMMENT_LENGTH = 28; protected static final int FIELD_COMMENT_POS = 97; protected static final int FIELD_TRACK_INDICATOR_LENGTH = 1; protected static final int FIELD_TRACK_INDICATOR_POS = 125; protected static final int FIELD_TRACK_LENGTH = 1; protected static final int FIELD_TRACK_POS = 126; /** * Track is held as a single byte in v1.1 */ protected byte track = (byte) TRACK_UNDEFINED; private static final byte RELEASE = 1; private static final byte MAJOR_VERSION = 1; private static final byte REVISION = 0; /** * Retrieve the Release */ public byte getRelease() { return RELEASE; } /** * Retrieve the Major Version */ public byte getMajorVersion() { return MAJOR_VERSION; } /** * Retrieve the Revision */ public byte getRevision() { return REVISION; } /** * Creates a new ID3v11 datatype. */ public ID3v11Tag() { } public int getFieldCount() { return 7; } public ID3v11Tag(ID3v11Tag copyObject) { super(copyObject); this.track = copyObject.track; } /** * Creates a new ID3v11 datatype from a non v11 tag * * @param mp3tag * @throws UnsupportedOperationException */ public ID3v11Tag(AbstractTag mp3tag) { if (mp3tag != null) { if (mp3tag instanceof ID3v1Tag) { if (mp3tag instanceof ID3v11Tag) { throw new UnsupportedOperationException("Copy Constructor not called. Please type cast the argument"); } // id3v1_1 objects are also id3v1 objects ID3v1Tag id3old = (ID3v1Tag) mp3tag; this.title = id3old.title; this.artist = id3old.artist; this.album = id3old.album; this.comment = id3old.comment; this.year = id3old.year; this.genre = id3old.genre; } else { ID3v24Tag id3tag; // first change the tag to ID3v2_4 tag if not one already if (!(mp3tag instanceof ID3v24Tag)) { id3tag = new ID3v24Tag(mp3tag); } else { id3tag = (ID3v24Tag) mp3tag; } ID3v24Frame frame; String text; if (id3tag.hasFrame(ID3v24Frames.FRAME_ID_TITLE)) { frame = (ID3v24Frame) id3tag.getFrame(ID3v24Frames.FRAME_ID_TITLE); text = ((FrameBodyTIT2) frame.getBody()).getText(); this.title = ID3Tags.truncate(text, FIELD_TITLE_LENGTH); } if (id3tag.hasFrame(ID3v24Frames.FRAME_ID_ARTIST)) { frame = (ID3v24Frame) id3tag.getFrame(ID3v24Frames.FRAME_ID_ARTIST); text = ((FrameBodyTPE1) frame.getBody()).getText(); this.artist = ID3Tags.truncate(text, FIELD_ARTIST_LENGTH); } if (id3tag.hasFrame(ID3v24Frames.FRAME_ID_ALBUM)) { frame = (ID3v24Frame) id3tag.getFrame(ID3v24Frames.FRAME_ID_ALBUM); text = ((FrameBodyTALB) frame.getBody()).getText(); this.album = ID3Tags.truncate(text, FIELD_ALBUM_LENGTH); } if (id3tag.hasFrame(ID3v24Frames.FRAME_ID_YEAR)) { frame = (ID3v24Frame) id3tag.getFrame(ID3v24Frames.FRAME_ID_YEAR); text = ((FrameBodyTDRC) frame.getBody()).getText(); this.year = ID3Tags.truncate(text, FIELD_YEAR_LENGTH); } if (id3tag.hasFrame(ID3v24Frames.FRAME_ID_COMMENT)) { Iterator iterator = id3tag.getFrameOfType(ID3v24Frames.FRAME_ID_COMMENT); text = ""; while (iterator.hasNext()) { Object o = iterator.next(); if (o instanceof ID3v24Frame) { frame = (ID3v24Frame) o; text += (((FrameBodyCOMM) frame.getBody()).getText() + " "); } } this.comment = ID3Tags.truncate(text, FIELD_COMMENT_LENGTH); } if (id3tag.hasFrame(ID3v24Frames.FRAME_ID_GENRE)) { frame = (ID3v24Frame) id3tag.getFrame(ID3v24Frames.FRAME_ID_GENRE); text = ((FrameBodyTCON) frame.getBody()).getText(); try { this.genre = (byte) ID3Tags.findNumber(text); } catch (Exception ex) { //logger.log(Level.WARNING, getLoggingFilename() + ":" + "Unable to convert TCON frame to format suitable for v11 tag", ex); this.genre = (byte) ID3v1Tag.GENRE_UNDEFINED; } } if (id3tag.hasFrame(ID3v24Frames.FRAME_ID_TRACK)) { frame = (ID3v24Frame) id3tag.getFrame(ID3v24Frames.FRAME_ID_TRACK); this.track = (byte) ((FrameBodyTRCK) frame.getBody()).getTrackNo().intValue(); } } } } /** * Creates a new ID3v11 datatype. * * @param file * @param loggingFilename * @throws TagNotFoundException * @throws IOException */ public ID3v11Tag(RandomAccessFile file, String loggingFilename) throws TagNotFoundException, IOException { setLoggingFilename(loggingFilename); FileChannel fc; ByteBuffer byteBuffer = ByteBuffer.allocate(TAG_LENGTH); fc = file.getChannel(); fc.position(file.length() - TAG_LENGTH); fc.read(byteBuffer); byteBuffer.flip(); read(byteBuffer); } /** * Creates a new ID3v11 datatype. * * @param file * @throws TagNotFoundException * @throws IOException * @deprecated use {@link #ID3v11Tag(RandomAccessFile, String)} instead */ public ID3v11Tag(RandomAccessFile file) throws TagNotFoundException, IOException { this(file, ""); } /** * Set Comment * * @param comment */ public void setComment(String comment) { if (comment == null) { throw new IllegalArgumentException(ErrorMessage.GENERAL_INVALID_NULL_ARGUMENT.getMsg()); } this.comment = ID3Tags.truncate(comment, FIELD_COMMENT_LENGTH); } /** * Get Comment * * @return comment */ public String getFirstComment() { return comment; } /** * Set the track, v11 stores track numbers in a single byte value so can only * handle a simple number in the range 0-255. * * @param trackValue */ public void setTrack(String trackValue) { int trackAsInt; //Try and convert String representation of track into an integer try { trackAsInt = Integer.parseInt(trackValue); } catch (NumberFormatException e) { trackAsInt = 0; } //This value cannot be held in v1_1 if ((trackAsInt > TRACK_MAX_VALUE) || (trackAsInt < TRACK_MIN_VALUE)) { this.track = (byte) TRACK_UNDEFINED; } else { this.track = (byte) Integer.parseInt(trackValue); } } /** * Return the track number as a String. * * @return track */ public String getFirstTrack() { return String.valueOf(track & BYTE_TO_UNSIGNED); } public void addTrack(String track) { setTrack(track); } public List<TagField> getTrack() { if (getFirst(FieldKey.TRACK).length() > 0) { ID3v1TagField field = new ID3v1TagField(ID3v1FieldKey.TRACK.name(), getFirst(FieldKey.TRACK)); return returnFieldToList(field); } else { return new ArrayList<TagField>(); } } public void setField(TagField field) { FieldKey genericKey = FieldKey.valueOf(field.getId()); if (genericKey == FieldKey.TRACK) { setTrack(field.toString()); } else { super.setField(field); } } public List<TagField> getFields(FieldKey genericKey) { if (genericKey == FieldKey.TRACK) { return getTrack(); } else { return super.getFields(genericKey); } } public String getFirst(FieldKey genericKey) { switch (genericKey) { case ARTIST: return getFirstArtist(); case ALBUM: return getFirstAlbum(); case TITLE: return getFirstTitle(); case GENRE: return getFirstGenre(); case YEAR: return getFirstYear(); case TRACK: return getFirstTrack(); case COMMENT: return getFirstComment(); default: return ""; } } public TagField getFirstField(String id) { List<TagField> results; if (FieldKey.TRACK.name().equals(id)) { results = getTrack(); if (results != null) { if (results.size() > 0) { return results.get(0); } } return null; } else { return super.getFirstField(id); } } public boolean isEmpty() { return track <= 0 && super.isEmpty(); } /** * Delete any instance of tag fields with this key * * @param genericKey */ public void deleteField(FieldKey genericKey) { if (genericKey == FieldKey.TRACK) { track = 0; } else { super.deleteField(genericKey); } } /** * Compares Object with this only returns true if both v1_1 tags with all * fields set to same value * * @param obj Comparing Object * @return */ public boolean equals(Object obj) { if (!(obj instanceof ID3v11Tag)) { return false; } ID3v11Tag object = (ID3v11Tag) obj; return this.track == object.track && super.equals(obj); } /** * Find identifier within byteBuffer to indicate that a v11 tag exists within the buffer * * @param byteBuffer * @return true if find header for v11 tag within buffer */ public boolean seek(ByteBuffer byteBuffer) { byte[] buffer = new byte[FIELD_TAGID_LENGTH]; // read the TAG value byteBuffer.get(buffer, 0, FIELD_TAGID_LENGTH); if (!(Arrays.equals(buffer, TAG_ID))) { return false; } // Check for the empty byte before the TRACK byteBuffer.position(FIELD_TRACK_INDICATOR_POS); if (byteBuffer.get() != END_OF_FIELD) { return false; } //Now check for TRACK if the next byte is also null byte then not v1.1 //tag, however this means cannot have v1_1 tag with track setField to zero/undefined //because on next read will be v1 tag. return byteBuffer.get() != END_OF_FIELD; } /** * Read in a tag from the ByteBuffer * * @param byteBuffer from where to read in a tag * @throws TagNotFoundException if unable to read a tag in the byteBuffer */ public void read(ByteBuffer byteBuffer) throws TagNotFoundException { if (!seek(byteBuffer)) { throw new TagNotFoundException("ID3v1 tag not found"); } //logger.finer("Reading v1.1 tag"); //Do single file read of data to cut down on file reads byte[] dataBuffer = new byte[TAG_LENGTH]; byteBuffer.position(0); byteBuffer.get(dataBuffer, 0, TAG_LENGTH); String encoding = TextEncoding.CHARSET_ISO_8859_1; title = Utils.getString(dataBuffer, FIELD_TITLE_POS, FIELD_TITLE_LENGTH, encoding).trim(); Matcher m = AbstractID3v1Tag.endofStringPattern.matcher(title); if (m.find()) { title = title.substring(0, m.start()); } artist = Utils.getString(dataBuffer, FIELD_ARTIST_POS, FIELD_ARTIST_LENGTH, encoding).trim(); m = AbstractID3v1Tag.endofStringPattern.matcher(artist); if (m.find()) { artist = artist.substring(0, m.start()); } album = Utils.getString(dataBuffer, FIELD_ALBUM_POS, FIELD_ALBUM_LENGTH, encoding).trim(); m = AbstractID3v1Tag.endofStringPattern.matcher(album); if (m.find()) { album = album.substring(0, m.start()); } year = Utils.getString(dataBuffer, FIELD_YEAR_POS, FIELD_YEAR_LENGTH, encoding).trim(); m = AbstractID3v1Tag.endofStringPattern.matcher(year); if (m.find()) { year = year.substring(0, m.start()); } comment = Utils.getString(dataBuffer, FIELD_COMMENT_POS, FIELD_COMMENT_LENGTH, encoding).trim(); m = AbstractID3v1Tag.endofStringPattern.matcher(comment); if (m.find()) { comment = comment.substring(0, m.start()); } track = dataBuffer[FIELD_TRACK_POS]; genre = dataBuffer[FIELD_GENRE_POS]; } /** * Write this representation of tag to the file indicated * * @param file that this tag should be written to * @throws IOException thrown if there were problems writing to the file */ public void write(RandomAccessFile file) throws IOException { //logger.info("Saving ID3v11 tag to file"); byte[] buffer = new byte[TAG_LENGTH]; int i; String str; byte[] bytes; delete(file); file.seek(file.length()); System.arraycopy(TAG_ID, FIELD_TAGID_POS, buffer, FIELD_TAGID_POS, TAG_ID.length); int offset = FIELD_TITLE_POS; if (TagOptionSingleton.getInstance().isId3v1SaveTitle()) { bytes = title.getBytes(TextEncoding.CHARSET_ISO_8859_1); System.arraycopy(bytes, 0, buffer, offset, Math.min(bytes.length, FIELD_TITLE_LENGTH)); } offset = FIELD_ARTIST_POS; if (TagOptionSingleton.getInstance().isId3v1SaveArtist()) { bytes = artist.getBytes(TextEncoding.CHARSET_ISO_8859_1); System.arraycopy(bytes, 0, buffer, offset, Math.min(bytes.length, FIELD_ARTIST_LENGTH)); } offset = FIELD_ALBUM_POS; if (TagOptionSingleton.getInstance().isId3v1SaveAlbum()) { bytes = album.getBytes(TextEncoding.CHARSET_ISO_8859_1); System.arraycopy(bytes, 0, buffer, offset, Math.min(bytes.length, FIELD_ALBUM_LENGTH)); } offset = FIELD_YEAR_POS; if (TagOptionSingleton.getInstance().isId3v1SaveYear()) { bytes = year.getBytes(TextEncoding.CHARSET_ISO_8859_1); System.arraycopy(bytes, 0, buffer, offset, Math.min(bytes.length, FIELD_YEAR_LENGTH)); } offset = FIELD_COMMENT_POS; if (TagOptionSingleton.getInstance().isId3v1SaveComment()) { bytes = comment.getBytes(TextEncoding.CHARSET_ISO_8859_1); System.arraycopy(bytes, 0, buffer, offset, Math.min(bytes.length, FIELD_COMMENT_LENGTH)); } offset = FIELD_TRACK_POS; buffer[offset] = track; // skip one byte extra blank for 1.1 definition offset = FIELD_GENRE_POS; if (TagOptionSingleton.getInstance().isId3v1SaveGenre()) { buffer[offset] = genre; } file.write(buffer); //logger.info("Saved ID3v11 tag to file"); } public void createStructure() { MP3File.getStructureFormatter().openHeadingElement(TYPE_TAG, getIdentifier()); //Header MP3File.getStructureFormatter().addElement(TYPE_TITLE, this.title); MP3File.getStructureFormatter().addElement(TYPE_ARTIST, this.artist); MP3File.getStructureFormatter().addElement(TYPE_ALBUM, this.album); MP3File.getStructureFormatter().addElement(TYPE_YEAR, this.year); MP3File.getStructureFormatter().addElement(TYPE_COMMENT, this.comment); MP3File.getStructureFormatter().addElement(TYPE_TRACK, this.track); MP3File.getStructureFormatter().addElement(TYPE_GENRE, this.genre); MP3File.getStructureFormatter().closeHeadingElement(TYPE_TAG); } }