package com.limegroup.gnutella.metadata; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.util.Iterator; import java.util.Vector; import com.limegroup.gnutella.ByteOrder; import de.vdheide.mp3.ID3v2; import de.vdheide.mp3.ID3v2Exception; import de.vdheide.mp3.ID3v2Frame; import de.vdheide.mp3.NoID3v2TagException; /** * Provides a utility method to read ID3 Tag information from MP3 * files and creates XMLDocuments from them. * * @author Sumeet Thadani */ public class MP3MetaData extends AudioMetaData { public MP3MetaData(File f) throws IOException { super(f); } /** * Returns ID3Data for the file. * * LimeWire would prefer to use ID3V2 tags, so we try to parse the ID3V2 * tags first, and then v1 to get any missing tags. */ protected void parseFile(File file) throws IOException { parseID3v2Data(file); MP3Info mp3Info = new MP3Info(file.getCanonicalPath()); setBitrate(mp3Info.getBitRate()); setLength((int)mp3Info.getLengthInSeconds()); parseID3v1Data(file); } /** * Parses the file's id3 data. */ private void parseID3v1Data(File file) { // not long enough for id3v1 tag? if(file.length() < 128) return; RandomAccessFile randomAccessFile = null; try { randomAccessFile = new RandomAccessFile(file, "r"); long length = randomAccessFile.length(); randomAccessFile.seek(length - 128); byte[] buffer = new byte[30]; // If tag is wrong, no id3v1 data. randomAccessFile.readFully(buffer, 0, 3); String tag = new String(buffer, 0, 3); if(!tag.equals("TAG")) return; // We have an ID3 Tag, now get the parts randomAccessFile.readFully(buffer, 0, 30); if (getTitle() == null || getTitle().equals("")) setTitle(getString(buffer, 30)); randomAccessFile.readFully(buffer, 0, 30); if (getArtist() == null || getArtist().equals("")) setArtist(getString(buffer, 30)); randomAccessFile.readFully(buffer, 0, 30); if (getAlbum() == null || getAlbum().equals("")) setAlbum(getString(buffer, 30)); randomAccessFile.readFully(buffer, 0, 4); if (getYear() == null || getYear().equals("")) setYear(getString(buffer, 4)); randomAccessFile.readFully(buffer, 0, 30); int commentLength; if (getTrack()==0 || getTrack()==-1){ if(buffer[28] == 0) { setTrack((short)ByteOrder.ubyte2int(buffer[29])); commentLength = 28; } else { setTrack((short)0); commentLength = 3; } if (getComment()==null || getComment().equals("")) setComment(getString(buffer, commentLength)); } // Genre randomAccessFile.readFully(buffer, 0, 1); if (getGenre() ==null || getGenre().equals("")) setGenre( MP3MetaData.getGenreString((short)ByteOrder.ubyte2int(buffer[0]))); } catch(IOException ignored) { } finally { if( randomAccessFile != null ) try { randomAccessFile.close(); } catch(IOException ignored) {} } } /** * Helper method to generate a string from an id3v1 filled buffer. */ private String getString(byte[] buffer, int length) { try { return new String(buffer, 0, getTrimmedLength(buffer, length), ISO_LATIN_1); } catch (UnsupportedEncodingException err) { // should never happen return null; } } /** * Generates ID3Data from id3v2 data in the file. */ private void parseID3v2Data(File file) { ID3v2 id3v2Parser = null; try { id3v2Parser = new ID3v2(file); } catch (ID3v2Exception idvx) { //can't go on return ; } catch (IOException iox) { return ; } Vector frames = null; try { frames = id3v2Parser.getFrames(); } catch (NoID3v2TagException ntx) { return ; } //rather than getting each frame indvidually, we can get all the frames //and iterate, leaving the ones we are not concerned with for(Iterator iter=frames.iterator() ; iter.hasNext() ; ) { ID3v2Frame frame = (ID3v2Frame)iter.next(); String frameID = frame.getID(); byte[] contentBytes = frame.getContent(); String frameContent = null; if (contentBytes.length > 0) { try { String enc = (frame.isISOLatin1()) ? ISO_LATIN_1 : UNICODE; frameContent = new String(contentBytes, enc).trim(); } catch (UnsupportedEncodingException err) { // should never happen } } if(frameContent == null || frameContent.trim().equals("")) continue; //check which tag we are looking at if(MP3DataEditor.TITLE_ID.equals(frameID)) setTitle(frameContent); else if(MP3DataEditor.ARTIST_ID.equals(frameID)) setArtist(frameContent); else if(MP3DataEditor.ALBUM_ID.equals(frameID)) setAlbum(frameContent); else if(MP3DataEditor.YEAR_ID.equals(frameID)) setYear(frameContent); else if(MP3DataEditor.COMMENT_ID.equals(frameID)) { //ID3v2 comments field has null separators embedded to encode //language etc, the real content begins after the last null byte[] bytes = frame.getContent(); int startIndex = 0; for(int i=bytes.length-1; i>= 0; i--) { if(bytes[i] != (byte)0) continue; //OK we are the the last 0 startIndex = i; break; } frameContent = new String(bytes, startIndex, bytes.length-startIndex).trim(); setComment(frameContent); } else if(MP3DataEditor.TRACK_ID.equals(frameID)) { try { setTrack(Short.parseShort(frameContent)); } catch (NumberFormatException ignored) {} } else if(MP3DataEditor.GENRE_ID.equals(frameID)) { //ID3v2 frame for genre has the byte used in ID3v1 encoded //within it -- we need to parse that out int startIndex = frameContent.indexOf("("); int endIndex = frameContent.indexOf(")"); int genreCode = -1; //Note: It's possible that the user entered her own genre in //which case there could be spurious braces, the id3v2 braces //enclose values between 0 - 127 // Custom genres are just plain text and default genres (known // from id3v1) are referenced with values enclosed by braces and // with optional refinements which I didn't implement here. // http://www.id3.org/id3v2.3.0.html#TCON if(startIndex > -1 && endIndex > -1 && startIndex < frameContent.length()) { //we have braces check if it's valid String genreByte = frameContent.substring(startIndex+1, endIndex); try { genreCode = Integer.parseInt(genreByte); } catch (NumberFormatException nfx) { genreCode = -1; } } if(genreCode >= 0 && genreCode <= 127) setGenre(MP3MetaData.getGenreString((short)genreCode)); else setGenre(frameContent); } else if (MP3DataEditor.LICENSE_ID.equals(frameID)) { setLicense(frameContent); } } } /** * Takes a short and returns the corresponding genre string */ public static String getGenreString(short genre) { switch(genre) { case 0: return "Blues"; case 1: return "Classic Rock"; case 2: return "Country"; case 3: return "Dance"; case 4: return "Disco"; case 5: return "Funk"; case 6: return "Grunge"; case 7: return "Hip-Hop"; case 8: return "Jazz"; case 9: return "Metal"; case 10: return "New Age"; case 11: return "Oldies"; case 12: return "Other"; case 13: return "Pop"; case 14: return "R & B"; case 15: return "Rap"; case 16: return "Reggae"; case 17: return "Rock"; case 18: return "Techno"; case 19: return "Industrial"; case 20: return "Alternative"; case 21: return "Ska"; case 22: return "Death Metal"; case 23: return "Pranks"; case 24: return "Soundtrack"; case 25: return "Euro-Techno"; case 26: return "Ambient"; case 27: return "Trip-Hop"; case 28: return "Vocal"; case 29: return "Jazz+Funk"; case 30: return "Fusion"; case 31: return "Trance"; case 32: return "Classical"; case 33: return "Instrumental"; case 34: return "Acid"; case 35: return "House"; case 36: return "Game"; case 37: return "Sound Clip"; case 38: return "Gospel"; case 39: return "Noise"; case 40: return "AlternRock"; case 41: return "Bass"; case 42: return "Soul"; case 43: return "Punk"; case 44: return "Space"; case 45: return "Meditative"; case 46: return "Instrumental Pop"; case 47: return "Instrumental Rock"; case 48: return "Ethnic"; case 49: return "Gothic"; case 50: return "Darkwave"; case 51: return "Techno-Industrial"; case 52: return "Electronic"; case 53: return "Pop-Folk"; case 54: return "Eurodance"; case 55: return "Dream"; case 56: return "Southern Rock"; case 57: return "Comedy"; case 58: return "Cult"; case 59: return "Gangsta"; case 60: return "Top 40"; case 61: return "Christian Rap"; case 62: return "Pop+Funk"; case 63: return "Jungle"; case 64: return "Native American"; case 65: return "Cabaret"; case 66: return "New Wave"; case 67: return "Psychadelic"; case 68: return "Rave"; case 69: return "Showtunes"; case 70: return "Trailer"; case 71: return "Lo-Fi"; case 72: return "Tribal"; case 73: return "Acid Punk"; case 74: return "Acid Jazz"; case 75: return "Polka"; case 76: return "Retro"; case 77: return "Musical"; case 78: return "Rock & Roll"; case 79: return "Hard Rock"; case 80: return "Folk"; case 81: return "Folk-Rock"; case 82: return "National Folk"; case 83: return "Swing"; case 84: return "Fast Fusion"; case 85: return "Bebob"; case 86: return "Latin"; case 87: return "Revival"; case 88: return "Celtic"; case 89: return "Bluegrass"; case 90: return "Avantgarde"; case 91: return "Gothic Rock"; case 92: return "Progressive Rock"; case 93: return "Psychedelic Rock"; case 94: return "Symphonic Rock"; case 95: return "Slow Rock"; case 96: return "Big Band"; case 97: return "Chorus"; case 98: return "Easy Listening"; case 99: return "Acoustic"; case 100: return "Humour"; case 101: return "Speech"; case 102: return "Chanson"; case 103: return "Opera"; case 104: return "Chamber Music"; case 105: return "Sonata"; case 106: return "Symphony"; case 107: return "Booty Bass"; case 108: return "Primus"; case 109: return "Porn Groove"; case 110: return "Satire"; case 111: return "Slow Jam"; case 112: return "Club"; case 113: return "Tango"; case 114: return "Samba"; case 115: return "Folklore"; case 116: return "Ballad"; case 117: return "Power Ballad"; case 118: return "Rhythmic Soul"; case 119: return "Freestyle"; case 120: return "Duet"; case 121: return "Punk Rock"; case 122: return "Drum Solo"; case 123: return "A capella"; case 124: return "Euro-House"; case 125: return "Dance Hall"; default: return ""; } } }