View Javadoc

1   /*
2    * Entagged Audio Tag library
3    * Copyright (c) 2003-2005 Christian Laireiter <liree@web.de>
4    * 
5    * This library is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU Lesser General Public
7    * License as published by the Free Software Foundation; either
8    * version 2.1 of the License, or (at your option) any later version.
9    *  
10   * This library is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13   * Lesser General Public License for more details.
14   * 
15   * You should have received a copy of the GNU Lesser General Public
16   * License along with this library; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18   */
19  package entagged.audioformats.mp3;
20  
21  import java.util.Arrays;
22  import java.util.Calendar;
23  import java.util.GregorianCalendar;
24  import java.util.HashMap;
25  import java.util.HashSet;
26  import java.util.Iterator;
27  
28  import entagged.audioformats.generic.TagField;
29  import entagged.audioformats.mp3.util.id3frames.TextId3Frame;
30  import entagged.audioformats.mp3.util.id3frames.TimeId3Frame;
31  
32  /***
33   * This class converts the fields (frames) from
34   * {@linkplain entagged.audioformats.mp3.Id3v2Tag tags} from an older to a newer
35   * version.<br>
36   * <p>
37   * The best way to describe its function is to show an example:<br>
38   * Let's convert an Id3v2.3 tag into the version 2.4 and illustrate the need of
39   * a special handler (like this class).<br>
40   * <p>
41   * A major change in the version switch 2.3-&gt;2.4 occured on the information
42   * of the recording time.<br>
43   * To represent a fully qualified date and time for that in the Id3v2.4 you will
44   * just need the field (frame) &quot;TDRC&quot; which is a textfield containing
45   * formatted date-time description. It allows mulitple time-patterns to be used,
46   * the most accurate variation is &quot;yyyy-MM-ddTHH:mm:ss&quot; with
47   * &quot;THH&quot; representing the hour out of 24.
48   * </p>
49   * <p>
50   * For the same information you need three different fields (frames) with
51   * Id3v2.3. They are
52   * <ul>
53   * <li><b>TIME</b>: &quot;HHMM&quot; storing the time whithin the recorded day</li>
54   * <li><b>TDAT</b>: &quot;DDMM&quot; storing the date whithin the year</li>
55   * <li><b>TYER</b>: &quot;yyyy&quot; storing the year</li>
56   * </ul>
57   * </p>
58   * <p>
59   * So you see, If we want to convert this recoding time, we can't just simply
60   * convert the names of the field. We must collect information and join the
61   * data.<br>
62   * Who knows what else must be handled.
63   * </p>
64   * </p>
65   * 
66   * @author Christian Laireiter
67   */
68  public final class Id3V2TagConverter {
69  
70  	/***
71  	 * This field maps the field names of the version 2 frames to the one of
72  	 * version 3.<br>
73  	 */
74  	private final static HashMap conversion22to23;
75  
76  	/***
77  	 * Field name of the &quot;TDAT&quot; field of version v2.3.
78  	 */
79  	public final static String DATE = "TDAT";
80  
81  	/***
82  	 * This field stores a set of field-names which will be discarded upon
83  	 * converstion to 2.4.<br>
84  	 * An example would be &quot;TRDA&quot;. It can take any data according to
85  	 * spec 2.3. Nothing you really could parse to a date information.
86  	 */
87  	private final static HashSet discard24;
88  
89  	/***
90  	 * Field name of the &quot;TDAT&quot; field of version v2.3.
91  	 */
92  	public final static String RECORD_DAT = "TRDA";
93  
94  	/***
95  	 * This field containts the frame names of those frames, which will be
96  	 * handled special by the v2.3 to v2.4 conversion.<be>
97  	 */
98  	private final static HashSet specialStore24;
99  
100 	/***
101 	 * Field name of the &quot;TIME&quot; field of version v2.3.
102 	 */
103 	public final static String TIME = "TIME";
104 
105 	/***
106 	 * Field name of the &quot;TYER&quot; field of version v2.3.
107 	 */
108 	public final static String YEAR = "TYER";
109 
110 	/***
111 	 * Field name of the &quot;TDRC&quot; field of version 2.4.
112 	 */
113 	public final static String RECORDING_TIME = "TDRC";
114 
115 	/*
116 	 * This static block initializes conversion22to23.
117 	 */
118 	static {
119 		conversion22to23 = new HashMap();
120 		String[] v22 = { "BUF", "CNT", "COM", "CRA", "CRM", "ETC", "EQU",
121 				"GEO", "IPL", "LNK", "MCI", "MLL", "PIC", "POP", "REV", "RVA",
122 				"SLT", "STC", "TAL", "TBP", "TCM", "TCO", "TCR", "TDA", "TDY",
123 				"TEN", "TFT", "TIM", "TKE", "TLA", "TLE", "TMT", "TOA", "TOF",
124 				"TOL", "TOR", "TOT", "TP1", "TP2", "TP3", "TP4", "TPA", "TPB",
125 				"TRC", "TRD", "TRK", "TSI", "TSS", "TT1", "TT2", "TT3", "TXT",
126 				"TXX", "TYE", "UFI", "ULT", "WAF", "WAR", "WAS", "WCM", "WCP",
127 				"WPB", "WXX" };
128 		String[] v23 = { "RBUF", "PCNT", "COMM", "AENC", "", "ETCO", "EQUA",
129 				"GEOB", "IPLS", "LINK", "MCDI", "MLLT", "APIC", "POPM", "RVRB",
130 				"RVAD", "SYLT", "SYTC", "TALB", "TBPM", "TCOM", "TCON", "TCOP",
131 				DATE, "TDLY", "TENC", "TFLT", TIME, "TKEY", "TLAN", "TLEN",
132 				"TMED", "TOPE", "TOFN", "TOLY", "TORY", "TOAL", "TPE1", "TPE2",
133 				"TPE3", "TPE4", "TPOS", "TPUB", "TSRC", RECORD_DAT, "TRCK",
134 				"TSIZ", "TSSE", "TIT1", "TIT2", "TIT3", "TEXT", "TXXX", YEAR,
135 				"UFID", "USLT", "WOAF", "WOAR", "WOAS", "WCOM", "WCOP", "WPUB",
136 				"WXXX" };
137 		for (int i = 0; i < v22.length; i++) {
138 			conversion22to23.put(v22[i], v23[i]);
139 		}
140 		specialStore24 = new HashSet(Arrays.asList(new String[] { TIME, YEAR,
141 				DATE }));
142 		discard24 = new HashSet(Arrays.asList(new String[] { RECORD_DAT }));
143 		discard24.addAll(specialStore24);
144 	}
145 
146 	/***
147 	 * This method will convert the given <code>tag</code> into the given
148 	 * Id3v2 version.<br>
149 	 * Allowed values for <code>targetVersion</code> are:<br>
150 	 * <ul>
151 	 * <li> {@link Id3v2Tag#ID3V23} </li>
152 	 * <li> {@link Id3v2Tag#ID3V24} </li>
153 	 * </ul>
154 	 * The {@linkplain Id3v2Tag#getRepresentedVersion() version} of the given
155 	 * <code>tag</code> must be lower than the one to be converted two.<br>
156 	 * <br>
157 	 * 
158 	 * @param tag
159 	 *            The tag to be converted.
160 	 * @param targetVersion
161 	 *            The version to be converted to.
162 	 * @return The converted tag.
163 	 */
164 	public static Id3v2Tag convert(Id3v2Tag tag, int targetVersion) {
165 		assert tag != null
166 				&& (targetVersion == Id3v2Tag.ID3V22
167 						|| targetVersion == Id3v2Tag.ID3V23 || targetVersion == Id3v2Tag.ID3V24)
168 				&& (tag.getRepresentedVersion() == Id3v2Tag.ID3V22
169 						|| tag.getRepresentedVersion() == Id3v2Tag.ID3V23 || tag
170 						.getRepresentedVersion() == Id3v2Tag.ID3V24);
171 		Id3v2Tag result = null;
172 		if (targetVersion <= tag.getRepresentedVersion()) {
173 			// return the given tag, since only upward conversion is implemented
174 			result = tag;
175 		} else {
176 			if (tag.getRepresentedVersion() < Id3v2Tag.ID3V23) {
177 				result = convert22to23(tag);
178 			}
179 			if (tag.getRepresentedVersion() < Id3v2Tag.ID3V24
180 					&& targetVersion <= Id3v2Tag.ID3V24) {
181 				// convert from Id3v2.3 to Id3v2.4
182 				result = convert23to24(result);
183 			}
184 		}
185 		assert result != null;
186 		return result;
187 	}
188 
189 	/***
190 	 * This method converts the given tag from Id3v2.2 to Id3v2.3.<br>
191 	 * 
192 	 * @param source
193 	 *            The tag to be converted.
194 	 * @return A new object containing the converted data.<br>
195 	 */
196 	private static Id3v2Tag convert22to23(Id3v2Tag source) {
197 		assert source != null
198 				&& source.getRepresentedVersion() == Id3v2Tag.ID3V22;
199 		Iterator fields = source.getFields();
200 		while (fields.hasNext()) {
201 			TagField current = (TagField) fields.next();
202 			String currentId = current.getId();
203 			String conv = (String) conversion22to23.get(currentId);
204 			if (currentId.equals(conv)) {
205 				fields.remove();
206 				if (current instanceof TextId3Frame) {
207 					source.add(new TextId3Frame(conv, ((TextId3Frame) current)
208 							.getContent()));
209 				}
210 			}
211 		}
212 		source.setRepresentedVersion(Id3v2Tag.ID3V23);
213 		return source;
214 	}
215 
216 	/***
217 	 * This method converts the given tag from Id3v2.3 to Id3v2.4.<br>
218 	 * 
219 	 * @param source
220 	 *            The tag to be converted.
221 	 * @return A new object containing the converted data.<br>
222 	 */
223 	private static Id3v2Tag convert23to24(Id3v2Tag source) {
224 		assert source != null
225 				&& source.getRepresentedVersion() == Id3v2Tag.ID3V22;
226 		Iterator fields = source.getFields();
227 		HashMap specialStore = new HashMap();
228 		while (fields.hasNext()) {
229 			TagField current = (TagField) fields.next();
230 			if (specialStore24.contains(current.getId())) {
231 				specialStore.put(current.getId(), current);
232 			}
233 			if (discard24.contains(current.getId())) {
234 				fields.remove();
235 			}
236 		}
237 		/*
238 		 * Now convert some Special Fields.
239 		 */
240 		// TDAT, TIME and TYEAR -> to -> TDRC
241 		TimeId3Frame tdrc = createTimeField((TextId3Frame) specialStore
242 				.get(DATE), (TextId3Frame) specialStore.get(TIME),
243 				(TextId3Frame) specialStore.get(YEAR));
244 		source.set(tdrc);
245 		source.setRepresentedVersion(Id3v2Tag.ID3V24);
246 		return source;
247 	}
248 
249 	/***
250 	 * This method creates a {@link TimeId3Frame} from given Textfields.<br>
251 	 * This is a convenience method for the conversion of Id3 version 2.3 tags
252 	 * into 2.4.<br>
253 	 * If all of the parameters are <code>null</code>, a timestamp with zero
254 	 * data will be returned.
255 	 * 
256 	 * @param tdat
257 	 *            The old TDAT field. Maybe <code>null</code>.
258 	 * @param time
259 	 *            The old TIME field. Maybe <code>null</code>
260 	 * @param tyer
261 	 *            The old TYER field. Maybe <code>null</code>
262 	 * @return A time field containing given data.
263 	 */
264 	private static TimeId3Frame createTimeField(TextId3Frame tdat,
265 			TextId3Frame time, TextId3Frame tyer) {
266 		TimeId3Frame result = null;
267 		Calendar calendar = new GregorianCalendar();
268 		calendar.clear();
269 		try {
270 			if (tdat != null) {
271 				if (tdat.getContent().length() == 4) {
272 					calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(tdat
273 							.getContent().substring(0, 2)));
274 					calendar.set(Calendar.MONTH, Integer.parseInt(tdat
275 							.getContent().substring(2, 4)) - 1);
276 				} else {
277 					System.err
278 							.println("Field TDAT ignroed, since it is not spec conform: \""
279 									+ tdat.getContent() + "\"");
280 				}
281 			}
282 			if (time != null) {
283 				if (time.getContent().length() == 4) {
284 					calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(time
285 							.getContent().substring(0, 2)));
286 					calendar.set(Calendar.MINUTE, Integer.parseInt(time
287 							.getContent().substring(2, 4)));
288 				} else {
289 					System.err
290 							.println("Field TIME ignroed, since it is not spec conform: \""
291 									+ time.getContent() + "\"");
292 				}
293 			}
294 			if (tyer != null) {
295 				if (tyer.getContent().length() == 4) {
296 					calendar.set(Calendar.YEAR, Integer.parseInt(tyer
297 							.getContent()));
298 				} else {
299 					System.err
300 							.println("Field TYER ignroed, since it is not spec conform: \""
301 									+ tyer.getContent() + "\"");
302 				}
303 			}
304 			result = new TimeId3Frame(RECORD_DAT, calendar);
305 		} catch (NumberFormatException e) {
306 			System.err.println("Numberformatexception occured "
307 					+ "in timestamp interpretation, date is set to zero.");
308 			e.printStackTrace();
309 			calendar.clear();
310 		}
311 		return result;
312 	}
313 
314 }