Source code: org/esau/ptarmigan/impl/filter/ID3v1Filter.java
1 /* $Header: /cvsroot/ptarmigan/ptarmigan/src/java/org/esau/ptarmigan/impl/filter/ID3v1Filter.java,v 1.4 2002/09/19 03:42:01 reedesau Exp $ */
2
3 package org.esau.ptarmigan.impl.filter;
4
5 import java.io.IOException;
6 import java.io.InputStream;
7 import java.io.File;
8 import java.io.ByteArrayInputStream;
9 import java.io.RandomAccessFile;
10 import java.text.ParseException;
11
12 import org.xml.sax.SAXException;
13
14 import org.apache.commons.logging.Log;
15 import org.apache.commons.logging.LogFactory;
16
17 import org.esau.ptarmigan.impl.RandomAccessFileSource;
18 import org.esau.ptarmigan.util.HelperDate;
19 import org.esau.ptarmigan.util.HelperID3;
20 import org.esau.ptarmigan.util.HelperIO;
21 import org.esau.ptarmigan.util.HelperMisc;
22 import org.esau.ptarmigan.util.HelperURL;
23
24 /**
25 * Extract metadata from an ID3v1 tag
26 * <p>
27 * It's a 128-byte block at the end of a file.
28 * <p>
29 * May appear on MP3, Ogg, Flac, and other files.
30 * <p>
31 * Note that it is unsupported with streams, at least for now. This
32 * is because of the overhead required in scanning to the end of the file.
33 *
34 * @author Reed Esau
35 * @version $Revision: 1.4 $ $Date: 2002/09/19 03:42:01 $
36 */
37 public final class ID3v1Filter extends BinaryFilter implements RandomAccessFileSource {
38
39 public ID3v1Filter() throws SAXException {
40 }
41
42 String getNamespaceURI() {
43 return NS_URI;
44 }
45
46 String getNamespacePrefix() {
47 return NS_PREFIX;
48 }
49
50 /**
51 * SAX-Invoked parse of a 'document' from an input stream.
52 * <p>
53 * Note that the caller must already have called this.getByteStream()
54 * to obtain the raw data for the tag.
55 * <p>
56 * This module creates the document from the parsed ID3V1 metadata.
57 */
58 public void doParse() throws SAXException, IOException, ParseException {
59 log.debug("doParse");
60
61 writeID3v1();
62
63 m_media_properties.getRange().reduceLength(ID3V1_TAG_LENGTH);
64 }
65
66
67 /** build the xml content */
68 void writeID3v1()
69 throws IOException, SAXException, ParseException {
70
71 final String local_name = "id3v1";
72 final String q_name = NS_PREFIX + ":" + local_name;
73
74 startElement(NS_URI, local_name, q_name, EMPTY_ATTRS);
75
76 byte[] buf = new byte[128];
77 read(buf, 0, 3);
78
79 // StringBuffer buf2 = new StringBuffer();
80 // for (int i = 0; i < 128; i++) {
81 // buf2.append(Integer.toHexString(buf[i])).append(" ");
82 // }
83 // log.debug("buf2=" + buf2.toString());
84
85 if (buf[0] != 'T' || buf[1] != 'A' || buf[2] != 'G')
86 throw new ParseException("missing 'TAG' at start of tag", 0); //TODO: supply the position
87
88 read(buf, 0, ITEM_TITLE_LENGTH);
89 maybeWrite(buf, ITEM_TITLE_LENGTH, "title");
90
91 read(buf, 0, ITEM_ARTIST_LENGTH);
92 maybeWrite(buf, ITEM_ARTIST_LENGTH, "artist");
93
94 read(buf, 0, ITEM_ALBUM_LENGTH);
95 maybeWrite(buf, ITEM_ALBUM_LENGTH, "album");
96
97 read(buf, 0, ITEM_YEAR_LENGTH);
98 String year = HelperDate.extractYear( new String(buf, 0, ITEM_YEAR_LENGTH) );
99 if (year != null)
100 write("year", year);
101
102 read(buf, 0, ITEM_COMMENT_LENGTH-1);
103 maybeWrite(buf, ITEM_COMMENT_LENGTH-1, "comment");
104 read(); // ignore last byte of comment (used to establish version)
105
106 int track = read();
107 if (track == -1)
108 throw new IOException("underflow on reading track no");
109 write("track", track);
110
111 int code = read();
112 if (code == -1)
113 throw new IOException("underflow on reading genre code");
114 String genre_str = HelperID3.getNullsoftGenre(code);
115 if (genre_str != null)
116 write("genre", genre_str);
117
118 endElement(NS_URI, local_name, q_name);
119 }
120
121 /**
122 * obtain the id3 tag as a stream of bytes, for handy parsing
123 *
124 * Many different filetypes may have an ID3v1 tag (128 bytes)
125 * attached to their end.
126 */
127 InputStream getByteStream(String system_id)
128 throws IOException, SAXException {
129
130 File file = HelperURL.systemIdToFile(system_id);
131 if (file == null) {
132 log.warn("could not resolve the system id to a local file");
133 return null;
134 }
135
136 if (log.isDebugEnabled ())
137 log.debug("getByteStream: file=" + file);
138
139 RandomAccessFile raf = null;
140 try {
141 raf = new RandomAccessFile(file, "r");
142
143 long length = raf.length();
144
145 if (length < ID3V1_TAG_LENGTH) {
146 log.debug("file too short to contain ID3v1 tag");
147 return null;
148 }
149
150 byte[] buf = new byte[128];
151
152 raf.seek( raf.length() - buf.length );
153 raf.read(buf);
154
155 InputStream is = null;
156
157 if (HelperMisc.startsWith(buf, 0, buf.length, ID3_MARKER)) {
158 log.debug("found ID3 tag");
159 is = new ByteArrayInputStream(buf);
160 }
161 return is;
162 }
163 finally {
164 HelperIO.safeClose(raf);
165 }
166 }
167
168
169 /** */
170 void maybeWrite(byte[] buf, int length, String xml_name)
171 throws IOException, SAXException {
172
173 if (buf[0] == '\0') // don't even try
174 return;
175
176 //TODO: figure out which encoding to use
177 String ENCODING = "ASCII"; // replaces extended chars with question mark
178 String str = new String(buf, ENCODING).trim();
179
180 str = HelperMisc.clean(str); // there might still be nul & ctrl garbage
181
182 if (str.length() > 0)
183 write(xml_name, str);
184 }
185
186 static final int ID3V1_TAG_LENGTH = 128;
187
188 static final int ITEM_TAG_LENGTH = 3;
189 static final int ITEM_TITLE_LENGTH = 30;
190 static final int ITEM_ARTIST_LENGTH = 30;
191 static final int ITEM_ALBUM_LENGTH = 30;
192 static final int ITEM_YEAR_LENGTH = 4;
193 static final int ITEM_COMMENT_LENGTH = 29;
194
195 //
196 // data members
197 //
198
199 File m_file;
200
201 //
202 // constants
203 //
204
205 static final byte[] ID3_MARKER = { 'T', 'A', 'G'};
206
207 static final String NS_URI = "http://esau.org/ns/ptarmigan/id3v1";
208 static final String NS_PREFIX = "v1";
209
210 /**
211 * logging object
212 */
213 static Log log = LogFactory.getLog(ID3v1Filter.class);
214 }
215
216 /*
217 PTARMIGAN MODIFIED BSD LICENSE
218
219 Copyright (c) 2002, Reed Esau (reed.esau@pobox.com) All rights reserved.
220
221 Redistribution and use in source and binary forms, with or without
222 modification, are permitted provided that the following conditions are
223 met:
224
225 Redistributions of source code must retain the above copyright notice,
226 this list of conditions and the following disclaimer.
227
228 Redistributions in binary form must reproduce the above copyright notice,
229 this list of conditions and the following disclaimer in the documentation
230 and/or other materials provided with the distribution.
231
232 Neither the name of the Ptarmigan Project
233 (http://ptarmigan.sourceforge.net) nor the names of its contributors may
234 be used to endorse or promote products derived from this software without
235 specific prior written permission.
236
237 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
238 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
239 THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
240 PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
241 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
242 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
243 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
244 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
245 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
246 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
247 POSSIBILITY OF SUCH DAMAGE.
248 */