2
* MusicTag Copyright (C)2003,2004
4
* This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser
5
* General Public License as published by the Free Software Foundation; either version 2.1 of the License,
6
* or (at your option) any later version.
8
* This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
9
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
* See the GNU Lesser General Public License for more details.
12
* You should have received a copy of the GNU Lesser General Public License along with this library; if not,
13
* you can get a copy from http://www.opensource.org/licenses/lgpl-license.php or write to the Free Software
14
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16
package org.jaudiotagger.tag.id3;
18
import org.jaudiotagger.audio.mp3.MP3File;
19
import org.jaudiotagger.audio.generic.Utils;
20
import org.jaudiotagger.tag.id3.framebody.*;
21
import org.jaudiotagger.tag.id3.valuepair.PictureTypes;
22
import org.jaudiotagger.tag.id3.valuepair.ImageFormats;
23
import org.jaudiotagger.tag.id3.valuepair.TextEncoding;
24
import org.jaudiotagger.tag.*;
25
import org.jaudiotagger.tag.datatype.DataTypes;
26
import org.jaudiotagger.tag.mp4.field.Mp4TagTextField;
27
import org.jaudiotagger.tag.mp4.Mp4FieldKey;
30
import java.nio.ByteBuffer;
31
import java.nio.channels.FileChannel;
32
import java.nio.channels.WritableByteChannel;
37
* This is the abstract base class for all ID3v2 tags.
39
* @author : Paul Taylor
40
* @author : Eric Farng
41
* @version $Id: AbstractID3v2Tag.java,v 1.30 2007/12/03 13:28:04 paultaylor Exp $
43
public abstract class AbstractID3v2Tag extends AbstractID3Tag implements Tag
45
protected static final String TYPE_HEADER = "header";
46
protected static final String TYPE_BODY = "body";
48
//Tag ID as held in file
49
protected static final byte[] TAG_ID = {'I', 'D', '3'};
51
//The tag header is the same for ID3v2 versions
52
public static final int TAG_HEADER_LENGTH = 10;
53
protected static final int FIELD_TAGID_LENGTH = 3;
54
protected static final int FIELD_TAG_MAJOR_VERSION_LENGTH = 1;
55
protected static final int FIELD_TAG_MINOR_VERSION_LENGTH = 1;
56
protected static final int FIELD_TAG_FLAG_LENGTH = 1;
57
protected static final int FIELD_TAG_SIZE_LENGTH = 4;
59
protected static final int FIELD_TAGID_POS = 0;
60
protected static final int FIELD_TAG_MAJOR_VERSION_POS = 3;
61
protected static final int FIELD_TAG_MINOR_VERSION_POS = 4;
62
protected static final int FIELD_TAG_FLAG_POS = 5;
63
protected static final int FIELD_TAG_SIZE_POS = 6;
65
protected static final int TAG_SIZE_INCREMENT = 100;
67
//The max size we try to write in one go to avoid out of memory errors (10mb)
68
private static final long MAXIMUM_WRITABLE_CHUNK_SIZE = 10000000;
71
* Map of all frames for this tag
73
public HashMap frameMap = null;
76
* Holds the ids of invalid duplicate frames
78
protected static final String TYPE_DUPLICATEFRAMEID = "duplicateFrameId";
79
protected String duplicateFrameId = "";
82
* Holds byte count of invalid duplicate frames
84
protected static final String TYPE_DUPLICATEBYTES = "duplicateBytes";
85
protected int duplicateBytes = 0;
88
* Holds byte count of empty frames
90
protected static final String TYPE_EMPTYFRAMEBYTES = "emptyFrameBytes";
91
protected int emptyFrameBytes = 0;
94
* Holds the size of the tag as reported by the tag header
96
protected static final String TYPE_FILEREADSIZE = "fileReadSize";
97
protected int fileReadSize = 0;
100
* Holds byte count of invalid frames
102
protected static final String TYPE_INVALIDFRAMEBYTES = "invalidFrameBytes";
103
protected int invalidFrameBytes = 0;
108
public AbstractID3v2Tag()
113
* This constructor is used when a tag is created as a duplicate of another
114
* tag of the same type and version.
116
protected AbstractID3v2Tag(AbstractID3v2Tag copyObject)
121
* Copy primitives apply to all tags
123
protected void copyPrimitives(AbstractID3v2Tag copyObject)
125
logger.info("Copying Primitives");
126
//Primitives type variables common to all IDv2 Tags
127
this.duplicateFrameId = new String(copyObject.duplicateFrameId);
128
this.duplicateBytes = copyObject.duplicateBytes;
129
this.emptyFrameBytes = copyObject.emptyFrameBytes;
130
this.fileReadSize = copyObject.fileReadSize;
131
this.invalidFrameBytes = copyObject.invalidFrameBytes;
135
* Copy frames from another tag, needs implemanting by subclasses
137
protected abstract void copyFrames(AbstractID3v2Tag copyObject);
141
* Returns the number of bytes which come from duplicate frames
143
* @return the number of bytes which come from duplicate frames
145
public int getDuplicateBytes()
147
return duplicateBytes;
151
* Return the string which holds the ids of all
154
* @return the string which holds the ids of all duplicate frames.
156
public String getDuplicateFrameId()
158
return duplicateFrameId;
162
* Returns the number of bytes which come from empty frames
164
* @return the number of bytes which come from empty frames
166
public int getEmptyFrameBytes()
168
return emptyFrameBytes;
172
* Return byte count of invalid frames
174
* @return byte count of invalid frames
176
public int getInvalidFrameBytes()
178
return invalidFrameBytes;
182
* Returns the tag size as reported by the tag header
184
* @return the tag size as reported by the tag header
186
public int getFileReadBytes()
192
* Return whether tag has frame with this identifier
194
* Warning the match is only done against the identifier so if a tag contains a frame with an unsuported body
195
* but happens to have an identifier that is valid for another version of the tag it will return true
197
* @param identifier frameId to lookup
198
* @return true if tag has frame with this identifier
200
public boolean hasFrame(String identifier)
202
return frameMap.containsKey(identifier);
207
* Return whether tag has frame with this identifier and a related body. This is required to protect
208
* against circumstances whereby a tag contains a frame with an unsupported body
209
* but happens to have an identifier that is valid for another version of the tag which it has been converted to
211
* e.g TDRC is an invalid frame in a v23 tag but if somehow a v23tag has been created by another application
212
* with a TDRC frame we construct an UnsupportedFrameBody to hold it, then this library constructs a
213
* v24 tag, it will contain a frame with id TDRC but it will not have the expected frame body it is not really a
216
* @param identifier frameId to lookup
217
* @return true if tag has frame with this identifier
219
public boolean hasFrameAndBody(String identifier)
221
if (hasFrame(identifier))
223
Object o = getFrame(identifier);
224
if (o instanceof AbstractID3v2Frame)
226
if (((AbstractID3v2Frame) o).getBody() instanceof FrameBodyUnsupported)
238
* Return whether tag has frame starting with this identifier
240
* Warning the match is only done against the identifier so if a tag contains a frame with an unsupported body
241
* but happens to have an identifier that is valid for another version of the tag it will return true
243
* @param identifier start of frameId to lookup
244
* @return tag has frame starting with this identifier
246
public boolean hasFrameOfType(String identifier)
248
Iterator iterator = frameMap.keySet().iterator();
250
boolean found = false;
251
while (iterator.hasNext() && !found)
253
key = (String) iterator.next();
254
if (key.startsWith(identifier))
264
* For single frames return the frame in this tag with given identifier if it exists, if multiple frames
265
* exist with the same identifier it will return a list containing all the frames with this identifier
267
* Warning the match is only done against the identifier so if a tag contains a frame with an unsupported body
268
* but happens to have an identifier that is valid for another version of the tag it will be returned.
271
* @param identifier is an ID3Frame identifier
272
* @return matching frame, or list of matching frames
274
//TODO:This method is problematic because sometimes it returns a list and sometimes a frame, we need to
275
//replace with two seperate methods as in the tag interface.
276
public Object getFrame(String identifier)
278
return frameMap.get(identifier);
282
* Retrieve the first value that exists for this identifier
284
* If the value is a String it returns that, otherwise returns a summary of the fields information
290
//TODO:we should be just be using the bodies toString() method so we dont have if statement in this method
291
//but this is being used by something else at the moment
292
public String getFirst(String identifier)
294
AbstractID3v2Frame frame = getFirstField(identifier);
299
if (frame.getBody() instanceof FrameBodyCOMM)
301
return ((FrameBodyCOMM) frame.getBody()).getText();
303
else if (frame.getBody() instanceof AbstractFrameBodyTextInfo)
305
return ((AbstractFrameBodyTextInfo) frame.getBody()).getFirstTextValue();
307
else if (frame.getBody() instanceof AbstractFrameBodyUrlLink)
309
return ((AbstractFrameBodyUrlLink) frame.getBody()).getUrlLink();
313
return frame.getBody().toString();
319
* Retrieve the first tagfield that exists for this identifier
322
* @return tag field or null if doesnt exist
324
public AbstractID3v2Frame getFirstField(String identifier)
326
Object object = getFrame(identifier);
331
if (object instanceof List)
333
return (AbstractID3v2Frame) ((List) object).get(0);
337
return (AbstractID3v2Frame) object;
342
* Add a frame to this tag
344
* @param frame the frame to add
347
* Warning if frame(s) already exists for this identifier thay are overwritten
350
//TODO needs to ensure do not add an invalid frame for this tag
351
//TODO what happens if already contains a list with this ID
352
public void setFrame(AbstractID3v2Frame frame)
354
frameMap.put(frame.getIdentifier(), frame);
357
protected abstract ID3Frames getID3Frames();
361
* @throws FieldDataInvalidException
363
public void set(TagField field) throws FieldDataInvalidException
365
if (!(field instanceof AbstractID3v2Frame))
367
throw new FieldDataInvalidException("Field " + field + " is not of type AbstractID3v2Frame");
370
AbstractID3v2Frame newFrame = (AbstractID3v2Frame) field;
372
Object o = frameMap.get(field.getId());
373
if (o == null || (!getID3Frames().isMultipleAllowed(newFrame.getId())))
375
System.out.println("Replacing....");
376
frameMap.put(field.getId(), field);
378
else if (o instanceof AbstractID3v2Frame)
380
System.out.println("Frame exists");
381
AbstractID3v2Frame oldFrame = (AbstractID3v2Frame) o;
382
if (newFrame.getBody() instanceof FrameBodyTXXX)
384
//Different key so convert to list and add as new frame
385
if (!((FrameBodyTXXX) newFrame.getBody()).getDescription()
386
.equals(((FrameBodyTXXX) oldFrame.getBody()).getDescription()))
388
List<AbstractID3v2Frame> frames = new ArrayList<AbstractID3v2Frame>();
389
frames.add(oldFrame);
390
frames.add(newFrame);
391
frameMap.put(newFrame.getId(), frames);
392
System.out.println("Adding....");
394
//Same key so replace
397
System.out.println("Replacing key....");
398
frameMap.put(newFrame.getId(), newFrame);
402
else if (o instanceof List)
404
for (ListIterator<AbstractID3v2Frame> li = ((List<AbstractID3v2Frame>) o).listIterator(); li.hasNext();)
406
AbstractID3v2Frame nextFrame = li.next();
408
if (newFrame.getBody() instanceof FrameBodyTXXX)
410
//Value with matching key exists so replace
411
if (((FrameBodyTXXX) newFrame.getBody()).getDescription()
412
.equals(((FrameBodyTXXX) nextFrame.getBody()).getDescription()))
415
frameMap.put(newFrame.getId(), o);
419
//No match found so add
420
((List<AbstractID3v2Frame>) o).add(newFrame);
424
public void setAlbum(String s) throws FieldDataInvalidException
426
set(createAlbumField(s));
429
public void setArtist(String s) throws FieldDataInvalidException
431
set(createArtistField(s));
434
public void setComment(String s) throws FieldDataInvalidException
436
set(createCommentField(s));
439
public void setGenre(String s) throws FieldDataInvalidException
441
set(createGenreField(s));
444
public void setTitle(String s) throws FieldDataInvalidException
446
set(createTitleField(s));
449
public void setTrack(String s) throws FieldDataInvalidException
451
set(createTrackField(s));
454
public void setYear(String s) throws FieldDataInvalidException
456
set(createYearField(s));
461
* @throws FieldDataInvalidException
463
public void add(TagField field) throws FieldDataInvalidException
470
if (!(field instanceof AbstractID3v2Frame))
472
throw new FieldDataInvalidException("Field " + field + " is not of type AbstractID3v2Frame");
475
Object o = frameMap.get(field.getId());
477
//There are already frames of this type
478
if (o instanceof List)
480
List list = (List) o;
483
//No frame of this type
486
frameMap.put(field.getId(), field);
488
//One frame exists, we are adding another so convert to list
491
List list = new ArrayList();
494
frameMap.put(field.getId(), list);
499
* Adds an album to the tag.<br>
501
* @param album Album description
503
public void addAlbum(String album) throws FieldDataInvalidException
505
add(createAlbumField(album));
509
* Adds an artist to the tag.<br>
511
* @param artist Artist's name
513
public void addArtist(String artist) throws FieldDataInvalidException
515
add(createArtistField(artist));
519
* Adds a comment to the tag.<br>
521
* @param comment Comment.
523
public void addComment(String comment) throws FieldDataInvalidException
525
add(createCommentField(comment));
529
* Adds a genre to the tag.<br>
533
public void addGenre(String genre) throws FieldDataInvalidException
535
add(createGenreField(genre));
539
* Adds a title to the tag.<br>
543
public void addTitle(String title) throws FieldDataInvalidException
545
add(createTitleField(title));
549
* Adds a track to the tag.<br>
553
public void addTrack(String track) throws FieldDataInvalidException
555
add(createTrackField(track));
559
* Adds a year to the Tag.<br>
563
public void addYear(String year) throws FieldDataInvalidException
565
add(createYearField(year));
570
* Used for setting multiple frames for a single frame Identifier
572
* Warning if frame(s) already exists for this identifier thay are overwritten
574
* TODO needs to ensure do not add an invalid frame for this tag
576
public void setFrame(String identifier, List<AbstractID3v2Frame> multiFrame)
578
frameMap.put(identifier, multiFrame);
582
* Return the number of frames in this tag of a particular type, multiple frames
583
* of the same time will only be counted once
585
* @return a count of different frames
587
public int getFrameCount()
589
if (frameMap == null)
595
return frameMap.size();
600
* Return all frames which start with the identifier, this
601
* can be more than one which is useful if trying to retrieve
602
* similar frames e.g TIT1,TIT2,TIT3 ... and don't know exaclty
603
* which ones there are.
605
* Warning the match is only done against the identifier so if a tag contains a frame with an unsupported body
606
* but happens to have an identifier that is valid for another version of the tag it will be returned.
609
* @return an iterator of all the frames starting with a particular identifier
611
public Iterator getFrameOfType(String identifier)
613
Iterator iterator = frameMap.keySet().iterator();
614
HashSet result = new HashSet();
616
while (iterator.hasNext())
618
key = (String) iterator.next();
619
if (key.startsWith(identifier))
621
result.add(frameMap.get(key));
624
return result.iterator();
631
* @param file to delete the tag from
632
* @throws IOException if problem accessing the file
635
//TODO should clear all data and preferably recover lost space.
636
public void delete(RandomAccessFile file) throws IOException
638
// this works by just erasing the "TAG" tag at the beginning
640
byte[] buffer = new byte[FIELD_TAGID_LENGTH];
641
//Read into Byte Buffer
642
final FileChannel fc = file.getChannel();
644
ByteBuffer byteBuffer = ByteBuffer.allocate(TAG_HEADER_LENGTH);
645
fc.read(byteBuffer, 0);
647
if (seek(byteBuffer))
655
* Is this tag equivalent to another
657
* @param obj to test for equivalence
658
* @return true if they are equivalent
660
public boolean equals(Object obj)
662
if ((obj instanceof AbstractID3v2Tag) == false)
666
AbstractID3v2Tag object = (AbstractID3v2Tag) obj;
667
if (this.frameMap.equals(object.frameMap) == false)
671
return super.equals(obj);
676
* Return the frames in the order they were added
678
* @return and iterator of the frmaes/list of multi value frames
680
public Iterator iterator()
682
return frameMap.values().iterator();
686
* Remove frame(s) with this identifier from tag
688
* @param identifier frameId to look for
690
public void removeFrame(String identifier)
692
logger.finest("Removing frame with identifier:" + identifier);
693
frameMap.remove(identifier);
697
* Remove all frame(s) which have an unsupported body, in other words
698
* remove all frames that are not part of the standard frameset for
701
public void removeUnsupportedFrames()
703
for (Iterator i = iterator(); i.hasNext();)
706
if (o instanceof AbstractID3v2Frame)
708
if (((AbstractID3v2Frame) o).getBody() instanceof FrameBodyUnsupported)
710
logger.finest("Removing frame" + ((AbstractID3v2Frame) o).getIdentifier());
718
* Remove any frames starting with this
719
* identifier from tag
721
* @param identifier start of frameId to look for
723
public void removeFrameOfType(String identifier)
725
Iterator iterator = this.getFrameOfType(identifier);
726
while (iterator.hasNext())
728
AbstractID3v2Frame frame = (AbstractID3v2Frame) iterator.next();
729
logger.finest("Removing frame with identifier:" + frame.getIdentifier() + "because starts with:" + identifier);
730
frameMap.remove(frame.getIdentifier());
740
* @param audioStartByte
741
* @throws IOException TODO should be abstract
743
public void write(File file, long audioStartByte) throws IOException
751
* @throws IOException TODO should be abstract
753
public void write(RandomAccessFile file) throws IOException
758
* Write tag to channel.
761
* @throws IOException TODO should be abstract
763
public void write(WritableByteChannel channel) throws IOException
769
* Checks to see if the file contains an ID3tag and if so return its size as reported in
770
* the tag header and return the size of the tag (including header), if no such tag exists return
774
* @return the end of the tag in the file or zero if no tag exists.
776
public static long getV2TagSizeIfExists(File file) throws IOException
778
FileInputStream fis = null;
779
FileChannel fc = null;
780
ByteBuffer bb = null;
784
fis = new FileInputStream(file);
785
fc = fis.getChannel();
787
//Read possible Tag header Byte Buffer
788
bb = ByteBuffer.allocate(TAG_HEADER_LENGTH);
791
if (bb.limit() < (TAG_HEADER_LENGTH))
810
byte[] tagIdentifier = new byte[FIELD_TAGID_LENGTH];
811
bb.get(tagIdentifier, 0, FIELD_TAGID_LENGTH);
812
if (!(Arrays.equals(tagIdentifier, TAG_ID)))
817
//Is it valid Major Version
818
byte majorVersion = bb.get();
819
if ((majorVersion != ID3v22Tag.MAJOR_VERSION) && (majorVersion != ID3v23Tag.MAJOR_VERSION) && (majorVersion != ID3v24Tag.MAJOR_VERSION))
830
//Get size as recorded in frame header
831
int frameSize = ID3SyncSafeInteger.bufferToValue(bb);
833
//add header size to frame size
834
frameSize += TAG_HEADER_LENGTH;
839
* Does a tag of the correct version exist in this file.
841
* @param byteBuffer to search through
842
* @return true if tag exists.
844
public boolean seek(ByteBuffer byteBuffer)
847
logger.info("ByteBuffer pos:" + byteBuffer.position() + ":limit" + byteBuffer.limit() + ":cap" + byteBuffer.capacity());
850
byte[] tagIdentifier = new byte[FIELD_TAGID_LENGTH];
851
byteBuffer.get(tagIdentifier, 0, FIELD_TAGID_LENGTH);
852
if (!(Arrays.equals(tagIdentifier, TAG_ID)))
857
if (byteBuffer.get() != getMajorVersion())
862
if (byteBuffer.get() != getRevision())
870
* This method determines the total tag size taking into account
871
* where the audio file starts, the size of the tagging data and
872
* user options for defining how tags should shrink or grow.
874
protected int calculateTagSize(int tagSize, int audioStart)
876
/** We can fit in the tag so no adjustments required */
877
if (tagSize <= audioStart)
881
/** There is not enough room as we need to move the audio file we might
882
* as well increase it more than neccessary for future changes
884
return tagSize + TAG_SIZE_INCREMENT;
888
* Adjust the length of the padding at the beginning of the MP3 file, this is only called when there is currently
889
* not enough space before the start of the audio to write the tag.
891
* A new file will be created with enough size to fit the <code>ID3v2</code> tag.
892
* The old file will be deleted, and the new file renamed.
894
* @param paddingSize This is total size required to store tag before audio
895
* @param file The file to adjust the padding length of
896
* @throws FileNotFoundException if the file exists but is a directory
897
* rather than a regular file or cannot be opened for any other
899
* @throws IOException on any I/O error
901
public void adjustPadding(File file, int paddingSize, long audioStart) throws FileNotFoundException, IOException
903
logger.finer("Need to move audio file to accomodate tag");
907
// Create buffer holds the neccessary padding
908
ByteBuffer paddingBuffer = ByteBuffer.wrap(new byte[paddingSize]);
910
// Create Temporary File and write channel
911
File paddedFile = File.createTempFile("temp", ".mp3", file.getParentFile());
912
fcOut = new FileOutputStream(paddedFile).getChannel();
914
//Create read channel from original file
915
fcIn = new FileInputStream(file).getChannel();
917
//Write padding to new file (this is where the tag will be written to later)
918
long written = (long) fcOut.write(paddingBuffer);
920
//Write rest of file starting from audio
921
logger.finer("Copying:" + (file.length() - audioStart) + "bytes");
923
//if the amount to be copied is very large we split into 10MB lumps to try and avoid
924
//out of memory errors
925
long audiolength = file.length() - audioStart;
926
if (audiolength <= MAXIMUM_WRITABLE_CHUNK_SIZE)
928
long written2 = fcIn.transferTo(audioStart, audiolength, fcOut);
929
logger.finer("Written padding:" + written + " Data:" + written2);
930
if (written2 != audiolength)
932
throw new RuntimeException("Problem adjusting padding, expecting to write:" + audiolength + ":only wrote:" + written2);
937
long noOfChunks = audiolength / MAXIMUM_WRITABLE_CHUNK_SIZE;
938
long lastChunkSize = audiolength % MAXIMUM_WRITABLE_CHUNK_SIZE;
940
for (int i = 0; i < noOfChunks; i++)
942
written2 += fcIn.transferTo(audioStart + (i * MAXIMUM_WRITABLE_CHUNK_SIZE), MAXIMUM_WRITABLE_CHUNK_SIZE, fcOut);
943
//Try and recover memory as quick as possible
944
Runtime.getRuntime().gc();
946
written2 += fcIn.transferTo(audioStart + (noOfChunks * MAXIMUM_WRITABLE_CHUNK_SIZE), lastChunkSize, fcOut);
947
logger.finer("Written padding:" + written + " Data:" + written2);
948
if (written2 != audiolength)
950
throw new RuntimeException("Problem adjusting padding in large file, expecting to write:" + audiolength + ":only wrote:" + written2);
954
//Store original modification time
955
long lastModified = file.lastModified();
961
//Delete original File
964
//Rename temporary file and set modification time to original time.
965
paddedFile.renameTo(file);
966
paddedFile.setLastModified(lastModified);
971
* Add frame to HashMap used when converting between tag versions, take into account
972
* occurences when two frame may both map to a single frame when converting between
975
* TODO the logic here is messy and seems to be specific to date fields only when it
976
* was intended to be generic.
978
protected void copyFrameIntoMap(String id, AbstractID3v2Frame newFrame)
980
/* The frame already exists this shouldnt normally happen because frames
981
* that are allowed to be multiple don't call this method. Frames that
982
* arent allowed to be multiple aren't added to hashmap in first place when
985
* We only want to allow one of the frames going forward but we try and merge
986
* all the information into the one frame. However there is a problem here that
987
* if we then take this, modify it and try to write back the original values
988
* we could lose some information although this info is probably invalid anyway.
990
* However converting some frames from tag of one version to another may
991
* mean that two different frames both get converted to one frame, this
992
* particulary applies to DateTime fields which were originally two fields
993
* in v2.3 but are one field in v2.4.
995
if (frameMap.containsKey(newFrame.getIdentifier()))
997
//Retrieve the frame with the same id we have already loaded into the map
998
AbstractID3v2Frame firstFrame = (AbstractID3v2Frame) frameMap.get(newFrame.getIdentifier());
1000
/* Two different frames both converted to TDRCFrames, now if this is the case one of them
1001
* may have actually have been created as a FrameUnsupportedBody because TDRC is only
1002
* supported in ID3v24, but is often created in v23 tags as well together with the valid TYER
1005
if (newFrame.getBody() instanceof FrameBodyTDRC)
1007
if (firstFrame.getBody() instanceof FrameBodyTDRC)
1009
logger.finest("Modifying frame in map:" + newFrame.getIdentifier());
1010
FrameBodyTDRC body = (FrameBodyTDRC) firstFrame.getBody();
1011
FrameBodyTDRC newBody = (FrameBodyTDRC) newFrame.getBody();
1012
//Just add the data to the frame
1013
if (newBody.getOriginalID().equals(ID3v23Frames.FRAME_ID_V3_TYER))
1015
body.setYear(newBody.getText());
1017
else if (newBody.getOriginalID().equals(ID3v23Frames.FRAME_ID_V3_TDAT))
1019
body.setDate(newBody.getText());
1021
else if (newBody.getOriginalID().equals(ID3v23Frames.FRAME_ID_V3_TIME))
1023
body.setTime(newBody.getText());
1025
else if (newBody.getOriginalID().equals(ID3v23Frames.FRAME_ID_V3_TRDA))
1027
body.setReco(newBody.getText());
1030
/* The first frame was a TDRC frame that was not really allowed, this new frame was probably a
1031
* valid frame such as TYER which has been converted to TDRC, replace the firstframe with this frame
1033
else if (firstFrame.getBody() instanceof FrameBodyUnsupported)
1035
frameMap.put(newFrame.getIdentifier(), newFrame);
1039
//we just lose this frame, weve already got one with the correct id.
1040
//TODO may want to store this somewhere
1041
logger.warning("Found duplicate TDRC frame in invalid situation,discarding:" + newFrame.getIdentifier());
1046
logger.warning("Found duplicate frame in invalid situation,discarding:" + newFrame.getIdentifier());
1050
//Just add frame to map
1052
logger.finest("Adding frame to map:" + newFrame.getIdentifier());
1053
frameMap.put(newFrame.getIdentifier(), newFrame);
1058
* Decides what to with the frame that has just be read from file.
1059
* If the frame is an allowable duplicate frame and is a duplicate we add all
1060
* frames into an ArrayList and add the Arraylist to the hashMap. if not allowed
1061
* to be duplicate we store bytes in the duplicateBytes variable.
1063
protected void loadFrameIntoMap(String frameId, AbstractID3v2Frame next)
1065
if ((ID3v24Frames.getInstanceOf().isMultipleAllowed(frameId)) || (ID3v23Frames.getInstanceOf().isMultipleAllowed(frameId)) || (ID3v22Frames.getInstanceOf().isMultipleAllowed(frameId)))
1067
//If a frame already exists of this type
1068
if (frameMap.containsKey(frameId))
1070
Object o = frameMap.get(frameId);
1071
if (o instanceof ArrayList)
1073
ArrayList multiValues = (ArrayList) o;
1074
multiValues.add(next);
1075
logger.finer("Adding Multi Frame(1)" + frameId);
1079
ArrayList multiValues = new ArrayList();
1081
multiValues.add(next);
1082
frameMap.put(frameId, multiValues);
1083
logger.finer("Adding Multi Frame(2)" + frameId);
1088
logger.finer("Adding Multi FrameList(3)" + frameId);
1089
frameMap.put(frameId, next);
1092
//If duplicate frame just stores it somewhere else
1093
else if (frameMap.containsKey(frameId))
1095
logger.warning("Duplicate Frame" + frameId);
1096
this.duplicateFrameId += (frameId + "; ");
1097
this.duplicateBytes += ((AbstractID3v2Frame) frameMap.get(frameId)).getSize();
1101
logger.finer("Adding Frame" + frameId);
1102
frameMap.put(frameId, next);
1107
* Return tag size based upon the sizes of the tags rather than the physical
1108
* no of bytes between start of ID3Tag and start of Audio Data.Should be extended
1109
* by subclasses to include header.
1111
* @return size of the tag
1113
public int getSize()
1116
Iterator iterator = frameMap.values().iterator();
1117
AbstractID3v2Frame frame;
1118
while (iterator.hasNext())
1120
Object o = iterator.next();
1121
if (o instanceof AbstractID3v2Frame)
1123
frame = (AbstractID3v2Frame) o;
1124
size += frame.getSize();
1128
ArrayList multiFrames = (ArrayList) o;
1129
for (ListIterator li = multiFrames.listIterator(); li.hasNext();)
1131
frame = (AbstractID3v2Frame) li.next();
1132
size += frame.getSize();
1140
* Write all the frames to the byteArrayOutputStream
1142
* <p>Currently Write all frames, defaults to the order in which they were loaded, newly
1143
* created frames will be at end of tag.
1145
* @return ByteBuffer Contains all the frames written within the tag ready for writing to file
1146
* @throws IOException
1148
//TODO there is a preferred tag order mentioned in spec, e.g ufid first
1149
protected ByteArrayOutputStream writeFramesToBuffer() throws IOException
1151
//Increases as is required
1152
ByteArrayOutputStream bodyBuffer = new ByteArrayOutputStream();
1155
AbstractID3v2Frame frame;
1157
iterator = frameMap.values().iterator();
1158
while (iterator.hasNext())
1160
Object o = iterator.next();
1161
if (o instanceof AbstractID3v2Frame)
1163
frame = (AbstractID3v2Frame) o;
1164
frame.write(bodyBuffer);
1168
ArrayList multiFrames = (ArrayList) o;
1169
for (ListIterator li = multiFrames.listIterator(); li.hasNext();)
1171
frame = (AbstractID3v2Frame) li.next();
1172
frame.write(bodyBuffer);
1181
public void createStructure()
1183
createStructureHeader();
1184
createStructureBody();
1187
public void createStructureHeader()
1189
MP3File.getStructureFormatter().addElement(this.TYPE_DUPLICATEBYTES, this.duplicateBytes);
1190
MP3File.getStructureFormatter().addElement(this.TYPE_DUPLICATEFRAMEID, this.duplicateFrameId);
1191
MP3File.getStructureFormatter().addElement(this.TYPE_EMPTYFRAMEBYTES, this.emptyFrameBytes);
1192
MP3File.getStructureFormatter().addElement(this.TYPE_FILEREADSIZE, this.fileReadSize);
1193
MP3File.getStructureFormatter().addElement(this.TYPE_INVALIDFRAMEBYTES, this.invalidFrameBytes);
1196
public void createStructureBody()
1198
MP3File.getStructureFormatter().openHeadingElement(TYPE_BODY, "");
1200
AbstractID3v2Frame frame;
1201
for (Object o : frameMap.values())
1203
if (o instanceof AbstractID3v2Frame)
1205
frame = (AbstractID3v2Frame) o;
1206
frame.createStructure();
1210
ArrayList multiFrames = (ArrayList) o;
1211
for (ListIterator li = multiFrames.listIterator(); li.hasNext();)
1213
frame = (AbstractID3v2Frame) li.next();
1214
frame.createStructure();
1218
MP3File.getStructureFormatter().closeHeadingElement(TYPE_BODY);
1222
* Retrieve the values that exists for this id3 frame id
1224
public List<TagField> get(String id) throws KeyNotFoundException
1226
Object o = getFrame(id);
1229
return new ArrayList<TagField>();
1231
else if (o instanceof List)
1233
//TODO should return copy
1236
else if (o instanceof AbstractID3v2Frame)
1238
List list = new ArrayList<TagField>();
1244
throw new RuntimeException("Found entry in frameMap that was not a frame or a list:" + o);
1248
public List getAlbum()
1250
return get(getAlbumId());
1253
public List getArtist()
1255
return get(getArtistId());
1258
public List getComment()
1260
return get(getCommentId());
1263
public List getGenre()
1265
return get(getGenreId());
1268
public List getTitle()
1270
return get(getTitleId());
1273
public List getTrack()
1275
return get(getTrackId());
1279
public List getYear()
1281
return get(getYearId());
1287
public String getFirstAlbum()
1289
List l = getAlbum();
1290
return (l.size() != 0) ? ((AbstractFrameBodyTextInfo) ((AbstractID3v2Frame) l.get(0)).getBody()).getText() : "";
1296
public String getFirstArtist()
1298
List l = getArtist();
1299
return (l.size() != 0) ? ((AbstractFrameBodyTextInfo) ((AbstractID3v2Frame) l.get(0)).getBody()).getText() : "";
1305
public String getFirstComment()
1307
List l = getComment();
1308
return (l.size() != 0) ? ((FrameBodyCOMM) ((AbstractID3v2Frame) l.get(0)).getBody()).getText() : "";
1314
public String getFirstGenre()
1316
List l = getGenre();
1317
return (l.size() != 0) ? ((AbstractFrameBodyTextInfo) ((AbstractID3v2Frame) l.get(0)).getBody()).getText() : "";
1323
public String getFirstTitle()
1325
List l = getTitle();
1326
return (l.size() != 0) ? ((AbstractFrameBodyTextInfo) ((AbstractID3v2Frame) l.get(0)).getBody()).getText() : "";
1332
public String getFirstTrack()
1334
List l = getTrack();
1335
return (l.size() != 0) ? ((AbstractFrameBodyTextInfo) ((AbstractID3v2Frame) l.get(0)).getBody()).getText() : "";
1341
public String getFirstYear()
1344
return (l.size() != 0) ? ((AbstractFrameBodyTextInfo) ((AbstractID3v2Frame) l.get(0)).getBody()).getText() : "";
1350
protected abstract String getArtistId();
1355
protected abstract String getAlbumId();
1360
protected abstract String getTitleId();
1365
protected abstract String getTrackId();
1370
protected abstract String getYearId();
1375
protected abstract String getCommentId();
1380
protected abstract String getGenreId();
1383
* Create Frame of correct ID3 version with the specified id
1388
public abstract AbstractID3v2Frame createFrame(String id);
1394
public TagField createArtistField(String content)
1396
AbstractID3v2Frame frame = createFrame(getArtistId());
1397
((AbstractFrameBodyTextInfo) frame.getBody()).setText(content);
1405
public TagField createAlbumField(String content)
1407
AbstractID3v2Frame frame = createFrame(getAlbumId());
1408
((AbstractFrameBodyTextInfo) frame.getBody()).setText(content);
1416
public TagField createTitleField(String content)
1418
AbstractID3v2Frame frame = createFrame(getTitleId());
1419
((AbstractFrameBodyTextInfo) frame.getBody()).setText(content);
1427
public TagField createTrackField(String content)
1429
AbstractID3v2Frame frame = createFrame(getTrackId());
1430
((AbstractFrameBodyTextInfo) frame.getBody()).setText(content);
1438
public TagField createYearField(String content)
1440
AbstractID3v2Frame frame = createFrame(getYearId());
1441
((AbstractFrameBodyTextInfo) frame.getBody()).setText(content);
1449
public TagField createCommentField(String content)
1451
AbstractID3v2Frame frame = createFrame(getCommentId());
1452
((FrameBodyCOMM) frame.getBody()).setText(content);
1460
public TagField createGenreField(String content)
1462
AbstractID3v2Frame frame = createFrame(getGenreId());
1463
((AbstractFrameBodyTextInfo) frame.getBody()).setText(content);
1468
public boolean hasCommonFields()
1474
* Does this tag contain a field with the specified id
1476
* @see org.jaudiotagger.tag.Tag#hasField(java.lang.String)
1478
public boolean hasField(String id)
1480
return get(id).size() != 0;
1486
* @see org.jaudiotagger.tag.Tag#isEmpty()
1488
public boolean isEmpty()
1490
return frameMap.size() == 0;
1494
public Iterator getFields()
1496
final Iterator<Map.Entry<String,Object>> it = this.frameMap.entrySet().iterator();
1497
return new Iterator()
1499
private Iterator fieldsIt;
1501
private void changeIt()
1508
Map.Entry<String,Object> e = it.next();
1509
if(e.getValue() instanceof List)
1511
List<TagField> l = (List)e.getValue();
1512
fieldsIt = l.iterator();
1516
//TODO must be a better way
1517
List<TagField> l = new ArrayList<TagField>();
1518
l.add((TagField)e.getValue());
1519
fieldsIt = l.iterator();
1523
public boolean hasNext()
1525
if (fieldsIt == null)
1529
return it.hasNext() || (fieldsIt != null && fieldsIt.hasNext());
1532
public Object next()
1534
if (!fieldsIt.hasNext())
1539
return fieldsIt.next();
1542
public void remove()
1549
public int getFieldCount()
1551
Iterator it = getFields();
1561
//TODO is this a special field?
1562
public boolean setEncoding(String enc) throws FieldDataInvalidException
1564
throw new UnsupportedOperationException("Not Implemented Yet");
1568
* Retrieve the first value that exists for this generic key
1573
public String getFirst(TagFieldKey genericKey) throws KeyNotFoundException
1575
if (genericKey == null)
1577
throw new KeyNotFoundException();
1580
return doGetFirst(getFrameAndSubIdFromGenericKey(genericKey));
1585
* Create a new TagField
1587
* Only textual data supported at the moment. The genericKey will be mapped
1588
* to the correct implementation key and return a TagField.
1590
* @param genericKey is the generic key
1591
* @param value to store
1594
public TagField createTagField(TagFieldKey genericKey, String value)
1595
throws KeyNotFoundException, FieldDataInvalidException
1597
if (genericKey == null)
1599
throw new KeyNotFoundException();
1601
return doCreateTagField(getFrameAndSubIdFromGenericKey(genericKey),value);
1605
* Create Frame for Id3 Key
1607
* Only textual data supported at the moment, should only be used with frames that
1608
* support a simple string argument.
1613
* @throws KeyNotFoundException
1614
* @throws FieldDataInvalidException
1616
protected TagField doCreateTagField(FrameAndSubId formatKey, String value)
1617
throws KeyNotFoundException, FieldDataInvalidException
1619
AbstractID3v2Frame frame = createFrame(formatKey.getFrameId());
1620
if (frame.getBody() instanceof FrameBodyUFID)
1622
((FrameBodyUFID) frame.getBody()).setOwner(formatKey.getSubId());
1625
((FrameBodyUFID) frame.getBody()).setUniqueIdentifier(value.getBytes("ISO-8859-1"));
1627
catch (UnsupportedEncodingException uee)
1629
//This will never happen because we are using a charset supported on all platforms
1631
throw new RuntimeException("When encoding UFID charset ISO-8859-1 was deemed unsupported");
1635
else if (frame.getBody() instanceof FrameBodyTXXX)
1637
((FrameBodyTXXX) frame.getBody()).setDescription(formatKey.getSubId());
1638
((FrameBodyTXXX) frame.getBody()).setText(value);
1641
else if (frame.getBody() instanceof FrameBodyWXXX)
1643
((FrameBodyWXXX) frame.getBody()).setDescription(formatKey.getSubId());
1644
((FrameBodyWXXX) frame.getBody()).setUrlLink(value);
1647
else if (frame.getBody() instanceof AbstractFrameBodyTextInfo)
1649
((AbstractFrameBodyTextInfo) frame.getBody()).setText(value);
1651
else if ((frame.getBody() instanceof FrameBodyAPIC )||(frame.getBody() instanceof FrameBodyPIC))
1653
throw new UnsupportedOperationException("Please use createArtwork() instead for creating artwork");
1657
throw new FieldDataInvalidException("Field with key of:" + formatKey.getFrameId() + ":does not accept cannot parse data:" + value);
1667
* @throws KeyNotFoundException
1669
protected String doGetFirst(FrameAndSubId formatKey) throws KeyNotFoundException
1671
//Simple 1 to 1 mapping
1672
if (formatKey.getSubId() == null)
1674
return getFirst(formatKey.getFrameId());
1678
//Get list of frames that this uses
1679
List<TagField> list = get(formatKey.getFrameId());
1680
ListIterator<TagField> li = list.listIterator();
1681
while (li.hasNext())
1683
AbstractTagFrameBody next = ((AbstractID3v2Frame) li.next()).getBody();
1684
if (next instanceof FrameBodyTXXX)
1686
if (((FrameBodyTXXX) next).getDescription().equals(formatKey.getSubId()))
1688
return ((FrameBodyTXXX) next).getText();
1691
else if (next instanceof FrameBodyWXXX)
1693
if (((FrameBodyWXXX) next).getDescription().equals(formatKey.getSubId()))
1695
return ((FrameBodyWXXX) next).getUrlLink();
1698
else if (next instanceof FrameBodyUFID)
1700
if (!((FrameBodyUFID) next).getUniqueIdentifier().equals(formatKey.getSubId()))
1702
return new String(((FrameBodyUFID) next).getUniqueIdentifier());
1707
throw new RuntimeException("Need to implement get(TagFieldKey genericKey) for:" + next.getClass());
1715
* Create a link to artwork, this is not recommended because the link may be broken if the mp3 or image
1718
* @param url specifies the link, it could be a local file or could be a full url
1721
public TagField createLinkedArtworkField(String url)
1723
AbstractID3v2Frame frame = createFrame(getFrameAndSubIdFromGenericKey(TagFieldKey.COVER_ART).getFrameId());
1724
if(frame.getBody() instanceof FrameBodyAPIC)
1726
FrameBodyAPIC body = (FrameBodyAPIC)frame.getBody();
1727
body.setObjectValue(DataTypes.OBJ_PICTURE_DATA, Utils.getDefaultBytes(url, TextEncoding.CHARSET_ISO_8859_1));
1728
body.setObjectValue(DataTypes.OBJ_PICTURE_TYPE, PictureTypes.DEFAULT_ID);
1729
body.setObjectValue(DataTypes.OBJ_MIME_TYPE, FrameBodyAPIC.IMAGE_IS_URL);
1730
body.setObjectValue(DataTypes.OBJ_DESCRIPTION, "");
1732
else if(frame.getBody() instanceof FrameBodyPIC)
1734
FrameBodyPIC body = (FrameBodyPIC)frame.getBody();
1735
body.setObjectValue(DataTypes.OBJ_PICTURE_DATA, Utils.getDefaultBytes(url, TextEncoding.CHARSET_ISO_8859_1));
1736
body.setObjectValue(DataTypes.OBJ_PICTURE_TYPE, PictureTypes.DEFAULT_ID);
1737
body.setObjectValue(DataTypes.OBJ_IMAGE_FORMAT, FrameBodyAPIC.IMAGE_IS_URL);
1738
body.setObjectValue(DataTypes.OBJ_DESCRIPTION, "");
1749
* @param mimeType of the image
1751
public TagField createArtworkField(byte[] data,String mimeType)
1753
AbstractID3v2Frame frame = createFrame(getFrameAndSubIdFromGenericKey(TagFieldKey.COVER_ART).getFrameId());
1754
if(frame.getBody() instanceof FrameBodyAPIC)
1756
FrameBodyAPIC body = (FrameBodyAPIC)frame.getBody();
1757
body.setObjectValue(DataTypes.OBJ_PICTURE_DATA, data);
1758
body.setObjectValue(DataTypes.OBJ_PICTURE_TYPE, PictureTypes.DEFAULT_ID);
1759
body.setObjectValue(DataTypes.OBJ_MIME_TYPE, mimeType);
1760
body.setObjectValue(DataTypes.OBJ_DESCRIPTION, "");
1762
else if(frame.getBody() instanceof FrameBodyPIC)
1764
FrameBodyPIC body = (FrameBodyPIC)frame.getBody();
1765
body.setObjectValue(DataTypes.OBJ_PICTURE_DATA, data);
1766
body.setObjectValue(DataTypes.OBJ_PICTURE_TYPE, PictureTypes.DEFAULT_ID);
1767
body.setObjectValue(DataTypes.OBJ_IMAGE_FORMAT, ImageFormats.getFormatForMimeType(mimeType));
1768
body.setObjectValue(DataTypes.OBJ_DESCRIPTION, "");
1774
* Delete fields with this generic key
1778
public void deleteTagField(TagFieldKey genericKey) throws KeyNotFoundException
1780
if (genericKey == null)
1782
throw new KeyNotFoundException();
1784
FrameAndSubId formatKey = getFrameAndSubIdFromGenericKey(genericKey);
1785
doDeleteTagField(formatKey);
1789
* Internal delete method
1792
* @throws KeyNotFoundException
1794
protected void doDeleteTagField(FrameAndSubId formatKey) throws KeyNotFoundException
1796
//Simple 1 to 1 mapping
1797
if (formatKey.getSubId() == null)
1799
removeFrame(formatKey.getFrameId());
1803
//Get list of frames that this uses
1804
List<TagField> list = get(formatKey.getFrameId());
1805
ListIterator<TagField> li = list.listIterator();
1806
while (li.hasNext())
1808
AbstractTagFrameBody next = ((AbstractID3v2Frame) li.next()).getBody();
1809
if (next instanceof FrameBodyTXXX)
1811
if (((FrameBodyTXXX) next).getDescription().equals(formatKey.getSubId()))
1816
else if (next instanceof FrameBodyWXXX)
1818
if (((FrameBodyWXXX) next).getDescription().equals(formatKey.getSubId()))
1823
else if (next instanceof FrameBodyUFID)
1825
if (((FrameBodyUFID) next).getUniqueIdentifier().equals(formatKey.getSubId()))
1832
throw new RuntimeException("Need to implement get(TagFieldKey genericKey) for:" + next.getClass());
1838
protected abstract FrameAndSubId getFrameAndSubIdFromGenericKey(TagFieldKey genericKey);
1841
* Get field(s) for this key
1845
* @throws KeyNotFoundException
1847
public List<TagField> get(TagFieldKey genericKey) throws KeyNotFoundException
1849
if (genericKey == null)
1851
throw new KeyNotFoundException();
1854
FrameAndSubId formatKey = getFrameAndSubIdFromGenericKey(genericKey);
1856
//Get list of frames that this uses, as we are going to remove entries we dont want take a copy
1857
List<TagField> list = get(formatKey.getFrameId());
1858
List<TagField> filteredList = new ArrayList<TagField>();
1859
String subFieldId = formatKey.getSubId();
1860
String frameid = formatKey.getFrameId();
1862
//... do we need to refine the list further i.e we only want TXXX frames that relate to the particular
1863
//key that was passed as a parameter
1864
if (subFieldId != null)
1866
for (TagField tagfield : list)
1868
AbstractTagFrameBody next = ((AbstractID3v2Frame) tagfield).getBody();
1869
if (next instanceof FrameBodyTXXX)
1871
if (((FrameBodyTXXX) next).getDescription().equals(formatKey.getSubId()))
1873
filteredList.add(tagfield);
1876
else if (next instanceof FrameBodyWXXX)
1878
if (((FrameBodyWXXX) next).getDescription().equals(formatKey.getSubId()))
1880
filteredList.add(tagfield);
1883
else if (next instanceof FrameBodyUFID)
1885
if (((FrameBodyUFID) next).getUniqueIdentifier().equals(formatKey.getSubId()))
1887
filteredList.add(tagfield);
1892
throw new RuntimeException("Need to implement get(TagFieldKey genericKey) for:" + next.getClass());
1895
return filteredList;
1904
* This class had to be created to minimize the duplicate code in concrete subclasses
1905
* of this class. It is required in some cases when using the Fieldkey enums because enums
1906
* cant be subclassed. We want to use enums instead of regular classes because they are
1907
* much easier for endusers to to use.
1911
private String frameId;
1912
private String subId;
1914
public FrameAndSubId(String frameId, String subId)
1916
this.frameId = frameId;
1920
public String getFrameId()
1925
public String getSubId()