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) {} } } } }