package com.limegroup.gnutella.metadata;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.limegroup.gnutella.util.FileUtils;
import com.limegroup.gnutella.xml.LimeXMLReplyCollection;
import com.limegroup.gnutella.xml.LimeXMLUtils;
import de.vdheide.mp3.ID3v2;
import de.vdheide.mp3.ID3v2DecompressionException;
import de.vdheide.mp3.ID3v2Exception;
import de.vdheide.mp3.ID3v2Frame;
import de.vdheide.mp3.NoID3v2TagException;
/**
* an editor specifically for mp3 files with id3 tags
*/
public class MP3DataEditor extends AudioMetaDataEditor {
private static final Log LOG =
LogFactory.getLog(MP3DataEditor.class);
private static final String ISO_LATIN_1 = "8859_1";
private static final String UNICODE = "Unicode";
static final String TITLE_ID = "TIT2";
static final String ARTIST_ID = "TPE1";
static final String ALBUM_ID = "TALB";
static final String YEAR_ID = "TYER";
static final String TRACK_ID = "TRCK";
static final String COMMENT_ID = "COMM";
static final String GENRE_ID = "TCON";
static final String LICENSE_ID = "TCOP";
/**
* Actually writes the ID3 tags out to the ID3V3 section of the mp3 file
*/
private int writeID3V2DataToDisk(File file) throws IOException, ID3v2Exception {
ID3v2 id3Handler = new ID3v2(file);
Vector frames = null;
try {
frames = (Vector)id3Handler.getFrames().clone();
} catch (NoID3v2TagException ex) {//there are no ID3v2 tags in the file
//fall thro' we'll deal with it later -- frames will be null
}
List framesToUpdate = new ArrayList();
addAllNeededFrames(framesToUpdate);
if(framesToUpdate.size() == 0) //we have nothing to update
return LimeXMLReplyCollection.NORMAL;
if(frames != null) { //old frames present, update the differnt ones
for(Iterator iter=frames.iterator(); iter.hasNext(); ) {
ID3v2Frame oldFrame = (ID3v2Frame)iter.next();
//note: equality of ID3v2Frame based on value of id
int index = framesToUpdate.indexOf(oldFrame);
ID3v2Frame newFrame = null;
if(index >=0) {
newFrame = (ID3v2Frame)framesToUpdate.remove(index);
if(Arrays.equals(oldFrame.getContent(),
newFrame.getContent()))
continue;//no need to update, skip this frame
}
//we are either going to replace it if it was changed, or remove
//it since there is no equivalent frame in the ones we need to
//update, this means the user probably removed it
id3Handler.removeFrame(oldFrame);
if(newFrame != null)
id3Handler.addFrame(newFrame);
}
}
//now we are left with the ones we need to add only, if there were no
//old tags this will be all the frames that need to get updated
for(Iterator iter = framesToUpdate.iterator(); iter.hasNext() ; ) {
ID3v2Frame frame = (ID3v2Frame)iter.next();
id3Handler.addFrame(frame);
}
id3Handler.update();
//No Exceptions? We are home
return LimeXMLReplyCollection.NORMAL;
}
private void addAllNeededFrames(List updateList) {
add(updateList, title_, TITLE_ID);
add(updateList, artist_, ARTIST_ID);
add(updateList, album_, ALBUM_ID);
add(updateList, year_, YEAR_ID);
add(updateList, track_, TRACK_ID);
add(updateList, comment_, COMMENT_ID);
add(updateList, genre_, GENRE_ID);
add(updateList, license_, LICENSE_ID);
}
private void add(List list, String data, String id) {
if(data != null && !data.equals("")) {
// genre needs to be updated.
if(id == GENRE_ID && getGenreByte() > -1)
data = "(" + getGenreByte() + ")" + data;
ID3v2Frame frame = makeFrame(id, data);
if(frame != null)
list.add(frame);
}
}
private ID3v2Frame makeFrame(String frameID, String value) {
boolean isISOLatin1 = true;
// Basic/ISO-Latin-1: 0x0000 ... 0x00FF
// Unicode: > 0x00FF ??? Even with 3byte chars?
for(int i = 0; i < value.length(); i++) {
if (value.charAt(i) > 0x00FF) {
isISOLatin1 = false;
break;
}
}
try {
return new ID3v2Frame(frameID,
value.getBytes((isISOLatin1) ? ISO_LATIN_1 : UNICODE),
true, //discard tag if it's altered/unrecognized
true, //discard tag if file altered/unrecognized
false,//read/write
ID3v2Frame.NO_COMPRESSION, //no compression
(byte)0,//no encryption
(byte)0, //no Group
isISOLatin1);
} catch(ID3v2DecompressionException cx) {
return null;
} catch (UnsupportedEncodingException err) {
return null;
}
}
/**
* Actually writes the ID3 tags out to the ID3V1 section of mp3 file.
*/
private int writeID3V1DataToDisk(RandomAccessFile file) {
byte[] buffer = new byte[30];//max buffer length...drop/pickup vehicle
//see if there are ID3 Tags in the file
String tag="";
try {
file.readFully(buffer,0,3);
tag = new String(buffer,0,3);
} catch(EOFException e) {
return LimeXMLReplyCollection.RW_ERROR;
} catch(IOException e) {
return LimeXMLReplyCollection.RW_ERROR;
}
//We are sure this is an MP3 file.Otherwise this method would never
//be called.
if(!tag.equals("TAG")) {
//Write the TAG
try {
byte[] tagBytes = "TAG".getBytes();//has to be len 3
file.seek(file.length()-128);//reset the file-pointer
file.write(tagBytes,0,3);//write these three bytes into the File
} catch(IOException ioe) {
return LimeXMLReplyCollection.BAD_ID3;
}
}
LOG.debug("about to start writing to file");
boolean b;
b = toFile(title_,30,file,buffer);
if(!b)
return LimeXMLReplyCollection.FAILED_TITLE;
b = toFile(artist_,30,file,buffer);
if(!b)
return LimeXMLReplyCollection.FAILED_ARTIST;
b = toFile(album_,30,file,buffer);
if(!b)
return LimeXMLReplyCollection.FAILED_ALBUM;
b = toFile(year_,4,file,buffer);
if(!b)
return LimeXMLReplyCollection.FAILED_YEAR;
//comment and track (a little bit tricky)
b = toFile(comment_,28,file,buffer);//28 bytes for comment
if(!b)
return LimeXMLReplyCollection.FAILED_COMMENT;
byte trackByte = (byte)-1;//initialize
try{
if (track_ == null || track_.equals(""))
trackByte = (byte)0;
else
trackByte = Byte.parseByte(track_);
} catch(NumberFormatException nfe) {
return LimeXMLReplyCollection.FAILED_TRACK;
}
try{
file.write(0);//separator b/w comment and track(track is optional)
file.write(trackByte);
} catch(IOException e) {
return LimeXMLReplyCollection.FAILED_TRACK;
}
//genre
byte genreByte= getGenreByte();
try {
file.write(genreByte);
} catch(IOException e) {
return LimeXMLReplyCollection.FAILED_GENRE;
}
//come this far means we are OK.
return LimeXMLReplyCollection.NORMAL;
}
private boolean toFile(String val, int maxLen, RandomAccessFile file, byte[] buffer) {
if (LOG.isDebugEnabled())
LOG.debug("writing value to file "+val);
byte[] fromString;
if (val==null || val.equals("")) {
fromString = new byte[maxLen];
Arrays.fill(fromString,0,maxLen,(byte)0);//fill it all with 0
} else {
try {
fromString = val.getBytes(ISO_LATIN_1);
} catch (UnsupportedEncodingException err) {
// Should never happen
return false;
}
}
int len = fromString.length;
if (len < maxLen) {
System.arraycopy(fromString,0,buffer,0,len);
Arrays.fill(buffer,len,maxLen,(byte)0);//fill the rest with 0s
} else//cut off the rest
System.arraycopy(fromString,0,buffer,0,maxLen);
try {
file.write(buffer,0,maxLen);
} catch (IOException e) {
return false;
}
return true;
}
private byte getGenreByte() {
if(genre_==null) return -1;
else if(genre_.equals("Blues")) return 0;
else if(genre_.equals("Classic Rock")) return 1;
else if(genre_.equals("Country")) return 2;
else if(genre_.equals("Dance")) return 3;
else if(genre_.equals("Disco")) return 4;
else if(genre_.equals("Funk")) return 5;
else if(genre_.equals("Grunge")) return 6;
else if(genre_.equals("Hop")) return 7;
else if(genre_.equals("Jazz")) return 8;
else if(genre_.equals("Metal")) return 9;
else if (genre_.equals("New Age")) return 10;
else if(genre_.equals("Oldies")) return 11;
else if(genre_.equals("Other")) return 12;
else if(genre_.equals("Pop")) return 13;
else if (genre_.equals("R & B")) return 14;
else if(genre_.equals("Rap")) return 15;
else if(genre_.equals("Reggae")) return 16;
else if(genre_.equals("Rock")) return 17;
else if(genre_.equals("Techno")) return 17;
else if(genre_.equals("Industrial")) return 19;
else if(genre_.equals("Alternative")) return 20;
else if(genre_.equals("Ska")) return 21;
else if(genre_.equals("Metal")) return 22;
else if(genre_.equals("Pranks")) return 23;
else if(genre_.equals("Soundtrack")) return 24;
else if(genre_.equals("Euro-Techno")) return 25;
else if(genre_.equals("Ambient")) return 26;
else if(genre_.equals("Trip-Hop")) return 27;
else if(genre_.equals("Vocal")) return 28;
else if (genre_.equals("Jazz+Funk")) return 29;
else if(genre_.equals("Fusion")) return 30;
else if(genre_.equals("Trance")) return 31;
else if(genre_.equals("Classical")) return 32;
else if(genre_.equals("Instrumental")) return 33;
else if(genre_.equals("Acid")) return 34;
else if(genre_.equals("House")) return 35;
else if(genre_.equals("Game")) return 36;
else if(genre_.equals("Sound Clip")) return 37;
else if(genre_.equals("Gospel")) return 38;
else if(genre_.equals("Noise")) return 39;
else if(genre_.equals("AlternRock")) return 40;
else if(genre_.equals("Bass")) return 41;
else if(genre_.equals("Soul")) return 42;
else if(genre_.equals("Punk")) return 43;
else if(genre_.equals("Space")) return 44;
else if(genre_.equals("Meditative")) return 45;
else if(genre_.equals("Instrumental Pop")) return 46;
else if(genre_.equals("Instrumental Rock")) return 47;
else if(genre_.equals("Ethnic")) return 48;
else if(genre_.equals("Gothic")) return 49;
else if(genre_.equals("Darkwave")) return 50;
else if(genre_.equals("Techno-Industrial")) return 51;
else if(genre_.equals("Electronic")) return 52;
else if(genre_.equals("Pop-Folk")) return 53;
else if(genre_.equals("Eurodance")) return 54;
else if(genre_.equals("Dream")) return 55;
else if(genre_.equals("Southern Rock")) return 56;
else if(genre_.equals("Comedy")) return 57;
else if(genre_.equals("Cult")) return 58;
else if(genre_.equals("Gangsta")) return 59;
else if(genre_.equals("Top 40")) return 60;
else if(genre_.equals("Christian Rap")) return 61;
else if(genre_.equals("Pop/Funk")) return 62;
else if(genre_.equals("Jungle")) return 63;
else if(genre_.equals("Native American")) return 64;
else if(genre_.equals("Cabaret")) return 65;
else if(genre_.equals("New Wave")) return 66;
else if(genre_.equals("Psychadelic")) return 67;
else if(genre_.equals("Rave")) return 68;
else if(genre_.equals("Showtunes")) return 69;
else if(genre_.equals("Trailer")) return 70;
else if(genre_.equals("Lo-Fi")) return 71;
else if(genre_.equals("Tribal")) return 72;
else if(genre_.equals("Acid Punk")) return 73;
else if(genre_.equals("Acid Jazz")) return 74;
else if(genre_.equals("Polka")) return 75;
else if(genre_.equals("Retro")) return 76;
else if(genre_.equals("Musical")) return 77;
else if(genre_.equals("Rock & Roll")) return 78;
else if(genre_.equals("Hard Rock")) return 79;
else if(genre_.equals("Folk")) return 80;
else if(genre_.equals("Folk-Rock")) return 81;
else if(genre_.equals("National Folk")) return 82;
else if(genre_.equals("Swing")) return 83;
else if(genre_.equals("Fast Fusion")) return 84;
else if(genre_.equals("Bebob")) return 85;
else if(genre_.equals("Latin")) return 86;
else if(genre_.equals("Revival")) return 87;
else if(genre_.equals("Celtic")) return 88;
else if(genre_.equals("Bluegrass")) return 89;
else if(genre_.equals("Avantgarde")) return 90;
else if(genre_.equals("Gothic Rock")) return 91;
else if(genre_.equals("Progressive Rock")) return 92;
else if(genre_.equals("Psychedelic Rock")) return 93;
else if(genre_.equals("Symphonic Rock")) return 94;
else if(genre_.equals("Slow Rock")) return 95;
else if(genre_.equals("Big Band")) return 96;
else if(genre_.equals("Chorus")) return 97;
else if(genre_.equals("Easy Listening")) return 98;
else if(genre_.equals("Acoustic")) return 99;
else if(genre_.equals("Humour")) return 100;
else if(genre_.equals("Speech")) return 101;
else if(genre_.equals("Chanson")) return 102;
else if(genre_.equals("Opera")) return 103;
else if(genre_.equals("Chamber Music")) return 104;
else if(genre_.equals("Sonata")) return 105;
else if(genre_.equals("Symphony")) return 106;
else if(genre_.equals("Booty Bass")) return 107;
else if(genre_.equals("Primus")) return 108;
else if(genre_.equals("Porn Groove")) return 109;
else if(genre_.equals("Satire")) return 110;
else if(genre_.equals("Slow Jam")) return 111;
else if(genre_.equals("Club")) return 112;
else if(genre_.equals("Tango")) return 113;
else if(genre_.equals("Samba")) return 114;
else if(genre_.equals("Folklore")) return 115;
else if(genre_.equals("Ballad")) return 116;
else if(genre_.equals("Power Ballad")) return 117;
else if(genre_.equals("Rhythmic Soul")) return 118;
else if(genre_.equals("Freestyle")) return 119;
else if(genre_.equals("Duet")) return 120;
else if(genre_.equals("Punk Rock")) return 121;
else if(genre_.equals("Drum Solo")) return 122;
else if(genre_.equals("A capella")) return 123;
else if(genre_.equals("Euro-House")) return 124;
else if(genre_.equals("Dance Hall")) return 125;
else return -1;
}
public int commitMetaData(String filename) {
if (LOG.isDebugEnabled())
LOG.debug("committing mp3 file");
if(! LimeXMLUtils.isMP3File(filename))
return LimeXMLReplyCollection.INCORRECT_FILETYPE;
File f= null;
RandomAccessFile file = null;
try {
try {
f = new File(filename);
FileUtils.setWriteable(f);
file = new RandomAccessFile(f,"rw");
} catch(IOException e) {
return LimeXMLReplyCollection.FILE_DEFECTIVE;
}
long length=0;
try{
length = file.length();
if(length < 128) //could not write - file too small
return LimeXMLReplyCollection.FILE_DEFECTIVE;
file.seek(length - 128);
} catch(IOException ee) {
return LimeXMLReplyCollection.RW_ERROR;
}
//1. Try to write out the ID3v2 data first
int ret = -1;
try {
ret = writeID3V2DataToDisk(f);
} catch (IOException iox ) {
return LimeXMLReplyCollection.RW_ERROR;
} catch (ID3v2Exception e) { //catches both ID3v2 related exceptions
ret = writeID3V1DataToDisk(file);
}
return ret;
}
finally {
if( file != null ) {
try {
file.close();
} catch(IOException ignored) {}
}
}
}
}