1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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->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) "TDRC" which is a textfield containing
45 * formatted date-time description. It allows mulitple time-patterns to be used,
46 * the most accurate variation is "yyyy-MM-ddTHH:mm:ss" with
47 * "THH" 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>: "HHMM" storing the time whithin the recorded day</li>
54 * <li><b>TDAT</b>: "DDMM" storing the date whithin the year</li>
55 * <li><b>TYER</b>: "yyyy" 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 "TDAT" 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 "TRDA". 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 "TDAT" 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 "TIME" field of version v2.3.
102 */
103 public final static String TIME = "TIME";
104
105 /***
106 * Field name of the "TYER" field of version v2.3.
107 */
108 public final static String YEAR = "TYER";
109
110 /***
111 * Field name of the "TDRC" field of version 2.4.
112 */
113 public final static String RECORDING_TIME = "TDRC";
114
115
116
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
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
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
239
240
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 }