/* * Entagged Audio Tag library * Copyright (c) 2003-2005 Christian Laireiter <liree@web.de> * * 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, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package entagged.audioformats.mp3; import java.util.Arrays; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import entagged.audioformats.generic.TagField; import entagged.audioformats.mp3.util.id3frames.TextId3Frame; import entagged.audioformats.mp3.util.id3frames.TimeId3Frame; /** * This class converts the fields (frames) from * {@linkplain entagged.audioformats.mp3.Id3v2Tag tags} from an older to a newer * version.<br> * <p> * The best way to describe its function is to show an example:<br> * Let's convert an Id3v2.3 tag into the version 2.4 and illustrate the need of * a special handler (like this class).<br> * <p> * A major change in the version switch 2.3->2.4 occured on the information * of the recording time.<br> * To represent a fully qualified date and time for that in the Id3v2.4 you will * just need the field (frame) "TDRC" which is a textfield containing * formatted date-time description. It allows mulitple time-patterns to be used, * the most accurate variation is "yyyy-MM-ddTHH:mm:ss" with * "THH" representing the hour out of 24. * </p> * <p> * For the same information you need three different fields (frames) with * Id3v2.3. They are * <ul> * <li><b>TIME</b>: "HHMM" storing the time whithin the recorded day</li> * <li><b>TDAT</b>: "DDMM" storing the date whithin the year</li> * <li><b>TYER</b>: "yyyy" storing the year</li> * </ul> * </p> * <p> * So you see, If we want to convert this recoding time, we can't just simply * convert the names of the field. We must collect information and join the * data.<br> * Who knows what else must be handled. * </p> * </p> * * @author Christian Laireiter */ public final class Id3V2TagConverter { /** * This field maps the field names of the version 2 frames to the one of * version 3.<br> */ private final static HashMap conversion22to23; /** * Field name of the "TDAT" field of version v2.3. */ public final static String DATE = "TDAT"; /** * This field stores a set of field-names which will be discarded upon * converstion to 2.4.<br> * An example would be "TRDA". It can take any data according to * spec 2.3. Nothing you really could parse to a date information. */ private final static HashSet discard24; /** * Field name of the "TDAT" field of version v2.3. */ public final static String RECORD_DAT = "TRDA"; /** * This field containts the frame names of those frames, which will be * handled special by the v2.3 to v2.4 conversion.<be> */ private final static HashSet specialStore24; /** * Field name of the "TIME" field of version v2.3. */ public final static String TIME = "TIME"; /** * Field name of the "TYER" field of version v2.3. */ public final static String YEAR = "TYER"; /** * Field name of the "TDRC" field of version 2.4. */ public final static String RECORDING_TIME = "TDRC"; /* * This static block initializes conversion22to23. */ static { conversion22to23 = new HashMap(); String[] v22 = { "BUF", "CNT", "COM", "CRA", "CRM", "ETC", "EQU", "GEO", "IPL", "LNK", "MCI", "MLL", "PIC", "POP", "REV", "RVA", "SLT", "STC", "TAL", "TBP", "TCM", "TCO", "TCR", "TDA", "TDY", "TEN", "TFT", "TIM", "TKE", "TLA", "TLE", "TMT", "TOA", "TOF", "TOL", "TOR", "TOT", "TP1", "TP2", "TP3", "TP4", "TPA", "TPB", "TRC", "TRD", "TRK", "TSI", "TSS", "TT1", "TT2", "TT3", "TXT", "TXX", "TYE", "UFI", "ULT", "WAF", "WAR", "WAS", "WCM", "WCP", "WPB", "WXX" }; String[] v23 = { "RBUF", "PCNT", "COMM", "AENC", "", "ETCO", "EQUA", "GEOB", "IPLS", "LINK", "MCDI", "MLLT", "APIC", "POPM", "RVRB", "RVAD", "SYLT", "SYTC", "TALB", "TBPM", "TCOM", "TCON", "TCOP", DATE, "TDLY", "TENC", "TFLT", TIME, "TKEY", "TLAN", "TLEN", "TMED", "TOPE", "TOFN", "TOLY", "TORY", "TOAL", "TPE1", "TPE2", "TPE3", "TPE4", "TPOS", "TPUB", "TSRC", RECORD_DAT, "TRCK", "TSIZ", "TSSE", "TIT1", "TIT2", "TIT3", "TEXT", "TXXX", YEAR, "UFID", "USLT", "WOAF", "WOAR", "WOAS", "WCOM", "WCOP", "WPUB", "WXXX" }; for (int i = 0; i < v22.length; i++) { conversion22to23.put(v22[i], v23[i]); } specialStore24 = new HashSet(Arrays.asList(new String[] { TIME, YEAR, DATE })); discard24 = new HashSet(Arrays.asList(new String[] { RECORD_DAT })); discard24.addAll(specialStore24); } /** * This method will convert the given <code>tag</code> into the given * Id3v2 version.<br> * Allowed values for <code>targetVersion</code> are:<br> * <ul> * <li> {@link Id3v2Tag#ID3V23} </li> * <li> {@link Id3v2Tag#ID3V24} </li> * </ul> * The {@linkplain Id3v2Tag#getRepresentedVersion() version} of the given * <code>tag</code> must be lower than the one to be converted two.<br> * <br> * * @param tag * The tag to be converted. * @param targetVersion * The version to be converted to. * @return The converted tag. */ public static Id3v2Tag convert(Id3v2Tag tag, int targetVersion) { assert tag != null && (targetVersion == Id3v2Tag.ID3V22 || targetVersion == Id3v2Tag.ID3V23 || targetVersion == Id3v2Tag.ID3V24) && (tag.getRepresentedVersion() == Id3v2Tag.ID3V22 || tag.getRepresentedVersion() == Id3v2Tag.ID3V23 || tag .getRepresentedVersion() == Id3v2Tag.ID3V24); Id3v2Tag result = null; if (targetVersion <= tag.getRepresentedVersion()) { // return the given tag, since only upward conversion is implemented result = tag; } else { if (tag.getRepresentedVersion() < Id3v2Tag.ID3V23) { result = convert22to23(tag); } if (tag.getRepresentedVersion() < Id3v2Tag.ID3V24 && targetVersion <= Id3v2Tag.ID3V24) { // convert from Id3v2.3 to Id3v2.4 result = convert23to24(result); } } assert result != null; return result; } /** * This method converts the given tag from Id3v2.2 to Id3v2.3.<br> * * @param source * The tag to be converted. * @return A new object containing the converted data.<br> */ private static Id3v2Tag convert22to23(Id3v2Tag source) { assert source != null && source.getRepresentedVersion() == Id3v2Tag.ID3V22; Iterator fields = source.getFields(); while (fields.hasNext()) { TagField current = (TagField) fields.next(); String currentId = current.getId(); String conv = (String) conversion22to23.get(currentId); if (currentId.equals(conv)) { fields.remove(); if (current instanceof TextId3Frame) { source.add(new TextId3Frame(conv, ((TextId3Frame) current) .getContent())); } } } source.setRepresentedVersion(Id3v2Tag.ID3V23); return source; } /** * This method converts the given tag from Id3v2.3 to Id3v2.4.<br> * * @param source * The tag to be converted. * @return A new object containing the converted data.<br> */ private static Id3v2Tag convert23to24(Id3v2Tag source) { assert source != null && source.getRepresentedVersion() == Id3v2Tag.ID3V22; Iterator fields = source.getFields(); HashMap specialStore = new HashMap(); while (fields.hasNext()) { TagField current = (TagField) fields.next(); if (specialStore24.contains(current.getId())) { specialStore.put(current.getId(), current); } if (discard24.contains(current.getId())) { fields.remove(); } } /* * Now convert some Special Fields. */ // TDAT, TIME and TYEAR -> to -> TDRC TimeId3Frame tdrc = createTimeField((TextId3Frame) specialStore .get(DATE), (TextId3Frame) specialStore.get(TIME), (TextId3Frame) specialStore.get(YEAR)); source.set(tdrc); source.setRepresentedVersion(Id3v2Tag.ID3V24); return source; } /** * This method creates a {@link TimeId3Frame} from given Textfields.<br> * This is a convenience method for the conversion of Id3 version 2.3 tags * into 2.4.<br> * If all of the parameters are <code>null</code>, a timestamp with zero * data will be returned. * * @param tdat * The old TDAT field. Maybe <code>null</code>. * @param time * The old TIME field. Maybe <code>null</code> * @param tyer * The old TYER field. Maybe <code>null</code> * @return A time field containing given data. */ private static TimeId3Frame createTimeField(TextId3Frame tdat, TextId3Frame time, TextId3Frame tyer) { TimeId3Frame result = null; Calendar calendar = new GregorianCalendar(); calendar.clear(); try { if (tdat != null) { if (tdat.getContent().length() == 4) { calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(tdat .getContent().substring(0, 2))); calendar.set(Calendar.MONTH, Integer.parseInt(tdat .getContent().substring(2, 4)) - 1); } else { System.err .println("Field TDAT ignroed, since it is not spec conform: \"" + tdat.getContent() + "\""); } } if (time != null) { if (time.getContent().length() == 4) { calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(time .getContent().substring(0, 2))); calendar.set(Calendar.MINUTE, Integer.parseInt(time .getContent().substring(2, 4))); } else { System.err .println("Field TIME ignroed, since it is not spec conform: \"" + time.getContent() + "\""); } } if (tyer != null) { if (tyer.getContent().length() == 4) { calendar.set(Calendar.YEAR, Integer.parseInt(tyer .getContent())); } else { System.err .println("Field TYER ignroed, since it is not spec conform: \"" + tyer.getContent() + "\""); } } result = new TimeId3Frame(RECORD_DAT, calendar); } catch (NumberFormatException e) { System.err.println("Numberformatexception occured " + "in timestamp interpretation, date is set to zero."); e.printStackTrace(); calendar.clear(); } return result; } }