Source code: com/tuneology/avm/FreeDb.java
1 /*
2 FreeDb.java
3
4 Copyright (C) 2002 Fran Taylor
5
6 This file is part of java-avm.
7
8 java-avm is free software; you can redistribute it and/or modify it
9 under the terms of the GNU Lesser General Public License as
10 published by the Free Software Foundation; either version 2 of the
11 License, or (at your option) any later version.
12
13 java-avm is distributed in the hope that it will be useful, but
14 WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Lesser General Public License for more details.
17
18 You should have received a copy of the GNU Lesser General Public
19 License along with java-avm; if not, write to the Free Software
20 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
21 USA
22
23 $Id: FreeDb.java,v 1.4 2002/11/02 19:27:45 xnarf Exp $
24
25 */
26
27 package com.tuneology.avm;
28
29 import java.io.*;
30 import java.net.*;
31 import java.util.*;
32
33 /**
34 * An API for the FreeDb song database.
35 * <p>
36 * This API is single threaded; if you want to use it in a multithreaded program,
37 * create an instance of the FreeDb object for each thread.
38 *
39 * @author Fran Taylor
40 *
41 * @version $Id: FreeDb.java,v 1.4 2002/11/02 19:27:45 xnarf Exp $
42 */
43
44 public class FreeDb {
45 /**
46 * A class to represent a FreeDb server site.
47 */
48 public static class Site {
49 /** The hostname of the server. */
50 public String host;
51 /** The description of the the server's location. */
52 public String location;
53 /** The server's latitude and longitude. */
54 public String position;
55 }
56 /** Create a FreeDb object, ready to make queries.
57 *
58 * @param host The hostname of the freedb server.
59 * @param appName The name of the application.
60 * @param version The version of the application.
61 * @param url The URL to use for the request. If null, uses "/~cddb/cddb.cgi".
62 * @param proxyHost The http proxy host (optional).
63 * @param proxyPort The http proxy port number.
64 * @param cacheDir A directory to use for storing cached files (optional).
65 */
66 public FreeDb(String host, String appName, String version, String url, String proxyHost, int proxyPort, File cacheDir) {
67 if (host == null) host = "freedb.freedb.org";
68
69 this.urlString = "http://" + host + ((url == null) ? "/~cddb/cddb.cgi" : url);
70 this.proxyHost = proxyHost;
71 this.proxyPort = proxyPort;
72 this.cacheDir = cacheDir;
73 String hostName;
74 try {
75 hostName = InetAddress.getLocalHost().getHostName();
76 } catch (Exception e) {
77 hostName = "localhost";
78 }
79 idStr = "&hello=" +
80 System.getProperty("user.name") + "+" + hostName + "+" + appName + "+" + version + "&proto=5";
81 }
82 /**
83 * Translated a discid value from an integer to an 8-digit hex number .
84 *
85 * @param discId the integer value.
86 * @return a hex string representing the value.
87 */
88 public static String getHexId(int discId) {
89 String retval = Long.toHexString(((long) discId) & 0xffffffffL);
90 for(int i = retval.length(); i < 8; i++) retval = "0" + retval;
91 return retval;
92 }
93 /**
94 * Returns a list of the possible entries for the disc.
95 * The user chooses one and then get() is called to fetch
96 * the cddb data.
97 *
98 * @param trackStr The list of tracks, formatted as required for freedb.
99 * @return The list of category entries for the disc.
100 * @throws IOException if there is an error reading from the network.
101 */
102 public String []getCateg(String trackStr) throws IOException {
103 // look up the disc in the cache first
104 if (cacheDir != null) {
105
106 }
107 // not there, now try the FreeDb database
108 String req = urlString + "?cmd=cddb+query+" + trackStr + idStr;
109 String[] retval = null;
110 URL url;
111 BufferedReader urlRdr = null;
112 logOutput = "request: \"" + req + "\"\n";
113 url = new URL(req);
114 urlRdr = new BufferedReader(new InputStreamReader(url.openStream()));
115 String str = urlRdr.readLine();
116 int code = Integer.parseInt(str.substring(0, 3));
117 logOutput += "response: \"" + str + "\"\n";
118 if (code == 200) {
119 retval = new String[1];
120 retval[0] = str.substring(4);
121 } else if ((code == 210) || (code == 211)) {
122 Vector v = new Vector();
123 while((str = urlRdr.readLine()) != null) {
124 logOutput += "response: \"" + str + "\"\n";
125 if (!str.equals("."))
126 v.add(str);
127 }
128 retval = new String[v.size()];
129 for(int i = 0; i < v.size(); i++) { retval[i] = (String) v.get(i); }
130 } else
131 throw new IOException(str);
132 urlRdr.close();
133 return retval;
134 }
135
136 /**
137 * Returns a list of available servers.
138 *
139 * @return An array containing the available servers.
140 * @throws IOException if there is an error reading from the network.
141 */
142 public Site[] getServers() throws IOException {
143 setProxy();
144 URL url = new URL(urlString + "?cmd=sites" + idStr);
145 BufferedReader urlRdr = new BufferedReader(new InputStreamReader(url.openStream()));
146 String str = urlRdr.readLine();
147 resetProxy();
148 int code = Integer.parseInt(str.substring(0, 3));
149 if (code != 210) {
150 throw new IOException(str);
151 }
152 Vector hosts = new Vector();
153 Vector locs = new Vector();
154 Vector poss = new Vector();
155 while((str = urlRdr.readLine()) != null) {
156 if (str.equals(".")) continue;
157 StringTokenizer tok = new StringTokenizer(str, " ");
158 String host = tok.nextToken();
159 String proto = tok.nextToken();
160 String port = tok.nextToken();
161 String req = tok.nextToken();
162 String lat = tok.nextToken();
163 String lng = tok.nextToken();
164 String loc = tok.nextToken("\n").trim();
165 if (proto.equals("http") && port.equals("80")) {
166 hosts.add(host);
167 locs.add(loc);
168 poss.add(lat + ", " + lng);
169 }
170 }
171 urlRdr.close();
172 Site[] retval = new Site[hosts.size()];
173 for(int i = 0; i < hosts.size(); i++) {
174 retval[i] = new Site();
175 retval[i].host = (String) hosts.get(i);
176 retval[i].location = (String) locs.get(i);
177 retval[i].position = (String) poss.get(i);
178 }
179 return retval;
180 }
181
182 /**
183 * Returns information about the album, given
184 * its discid key,
185 *
186 * @param categ the category string.
187 * @param ent the entry to fill in.
188 * @return true if the information was found.
189 * @throws IOException if there is an error reading from the network.
190 */
191 public boolean get(String categStr, DiscInfo ent) throws IOException {
192 String discId = getHexId(ent.discId);
193 stuff = "";
194 // try looking in the cache directory first
195 StringTokenizer tok = new StringTokenizer(categStr, " ");
196 String categ = tok.nextToken();
197 String remoteId = tok.nextToken();
198 InputStream strm = getCacheFile(discId, categ);
199 boolean usingRemote = false;
200 // if it's not there, try the remote database
201 OutputStreamWriter cacheWriter = null;
202 File cacheFile = null;
203 if (strm == null) {
204 setProxy();
205 usingRemote = true;
206 strm = getRemoteData(remoteId, categ);
207 // cache the result if we have a place to put it
208 if (cacheDir != null) {
209 cacheFile = new File(cacheDir, discId + "-" + categ);
210 cacheWriter = new OutputStreamWriter(new FileOutputStream(cacheFile));
211 }
212 }
213 // read the results
214 BufferedReader rdr = new BufferedReader(new InputStreamReader(strm));
215 String str = rdr.readLine();
216 if (cacheWriter != null) cacheWriter.write(str + "\n");
217 if (usingRemote) resetProxy();
218 StringTokenizer rtok = new StringTokenizer(str, " ");
219 int code = Integer.parseInt(rtok.nextToken());
220 if (code != 210) {
221 if (cacheWriter != null) {
222 cacheWriter.close();
223 cacheFile.delete();
224 }
225 return false;
226 }
227 clearTrackNames(ent);
228 while((str = rdr.readLine()) != null) {
229 logOutput += str + "\n";
230 parseResult(ent, str);
231 if (cacheWriter != null)
232 cacheWriter.write(str + "\n");
233 }
234 rdr.close();
235 if (cacheWriter != null)
236 cacheWriter.close();
237 ent.comment = parseComment(ent.comment);
238 for(int i = 0; i < ent.tocs.length; i++) {
239 ent.tocs[i].comment = parseComment(ent.tocs[i].comment);
240 }
241 if (ent.genre.equals("")) ent.genre = capitalize(categ);
242 String sepChar = getSepChar(ent.tocs);
243 fixTracks(ent, sepChar);
244 ent.initialized = true;
245 return true;
246 }
247 /**
248 * Returns a good guess for the character that separates
249 * the artist from the title in the information received from
250 * freedb regarding multi-artist CDs.
251 *
252 * @param tracks the array of tracks.
253 * @return the separator character.
254 */
255 public static String getSepChar(TocEntry[] tracks) {
256 if (isSepChar(tracks, "/")) return "/";
257 if (isSepChar(tracks, "-")) return "-";
258 if (isSepChar(tracks, ":")) return ":";
259 return "/";
260 }
261 /**
262 * Returns the latest query and the result returned.
263 *
264 * @return the output of the network request.
265 */
266 public String getLogOutput() { return logOutput; }
267 private void setProxy() {
268 if ((proxyHost != null) && !proxyHost.equals("")) {
269 usingProxy = true;
270 systemProperties = System.getProperties();
271 savedProxySet = systemProperties.getProperty("proxySet");
272 savedProxyPort = systemProperties.getProperty("proxyPort");
273 savedProxyHost = systemProperties.getProperty("proxyHost");
274 systemProperties.setProperty("proxySet", "true");
275 systemProperties.setProperty("proxyHost", proxyHost);
276 systemProperties.setProperty("proxyPort", Integer.toString(proxyPort));
277 } else
278 usingProxy = false;
279 }
280 private void resetProxy() {
281 if (usingProxy) {
282 systemProperties.setProperty("proxySet", savedProxySet);
283 systemProperties.setProperty("proxyHost", savedProxyHost);
284 systemProperties.setProperty("proxyPort", savedProxyPort);
285 }
286 }
287 private String parseComment(String str) {
288 if (str == null) return null;
289 int pos;
290 StringBuffer buf = new StringBuffer(str);
291 while((pos = buf.toString().indexOf("\\n")) != -1) {
292 buf.replace(pos, pos + 2, "\n");
293 }
294 while((pos = buf.toString().indexOf("\\t")) != -1) {
295 buf.replace(pos, pos + 2, "\t");
296 }
297 return buf.toString();
298 }
299 private String capitalize(String str) {
300 int len = str.length();
301 if (len == 0) return str;
302 if (len == 1) return str.toUpperCase();
303 return str.substring(0, 1).toUpperCase() + str.substring(1);
304 }
305 private void parseResult(DiscInfo ent, String str) {
306 if (str.startsWith("DTITLE")) {
307 String entry = str.substring(str.indexOf("=") + 1);
308 int p = entry.indexOf("/");
309 ent.artist = entry.substring(0, p).trim();
310 if (ent.artist.equals("Various Artists") || ent.artist.equals("Soundtrack")) ent.multiArtist = true;
311 ent.album = entry.substring(p + 1).trim();
312 } else if (str.startsWith("DYEAR")) {
313 String yr = str.substring(str.indexOf("=") + 1);
314 try { ent.year = Integer.parseInt(yr.trim()); } catch (Exception e) { }
315 } else if (str.startsWith("DGENRE")) {
316 ent.genre = capitalize(str.substring(str.indexOf("=") + 1));
317 } else if (str.startsWith("TTITLE")) {
318 int eq = str.indexOf("=");
319 String trackTitle = str.substring(eq + 1);
320 String trk = str.substring(6, eq);
321 int track = Integer.parseInt(trk);
322 if (track < ent.tocs.length) {
323 ent.tocs[track].title += trackTitle;
324 if (ent.tocs[track].title.equals("")) ent.tocs[track].title = "Untitled";
325 }
326 } else if (str.startsWith("EXTD")) {
327 String comment = str.substring(str.indexOf("=") + 1);
328 if (comment != null)
329 ent.comment += stuff + comment;
330 stuff = "";
331 } else if (str.startsWith("EXTT")) {
332 int pos = str.indexOf("=");
333 String trk = str.substring(4, pos);
334 int track = Integer.parseInt(trk);
335 String comment = stuff + str.substring(pos + 1);
336 stuff = "";
337 if (track < ent.tocs.length) {
338 ent.tocs[track].comment += comment;
339 }
340 } else if (str.startsWith("DISCID")) {
341 } else if (str.startsWith("PLAYORDER")) {
342 } else {
343 if (!str.startsWith("#")) stuff = (str != null) ? str : "";
344 }
345 }
346 private void clearTrackNames(DiscInfo ent) {
347 TocEntry[] tracks = ent.tocs;
348 for(int i = 0; i < tracks.length; i++) {
349 tracks[i].title = "";
350 }
351 }
352 private InputStream getCacheFile(String discId, String categ) {
353 if (cacheDir == null) return null;
354 File cacheFile = new File(cacheDir, discId + "-" + categ);
355 if (!cacheFile.exists()) return null;
356 try {
357 return new FileInputStream(cacheFile);
358 } catch (FileNotFoundException ex) {
359 return null;
360 }
361 }
362 private InputStream getRemoteData(String discId, String categ) {
363 String req = urlString + "?cmd=cddb+read+" + categ + "+" + discId + idStr;
364 logOutput = "request: \"" + req + "\"" + "\n";
365 try {
366 URL url = new URL(req);
367 return url.openStream();
368 } catch (Exception ex) {
369 return null;
370 }
371 }
372 private void fixTracks(DiscInfo ent, String sepChar) {
373 for(int i = 0; i < ent.tocs.length; i++) {
374 TocEntry track = ent.tocs[i];
375 track.album = ent.album;
376 String str = ent.tocs[i].title;
377 if (str == null) str = "";
378 int ind;
379 if (ent.multiArtist && ((ind = str.lastIndexOf(sepChar)) != -1)) {
380 track.artist = str.substring(0, ind).trim();
381 track.title = str.substring(ind + 1).trim();
382 } else {
383 track.artist = ent.artist;
384 track.title = str;
385 }
386 if (!ent.genre.equals("")) track.genre = ent.genre;
387 if (ent.year != 0) track.year = ent.year;
388 }
389 }
390 private static boolean isSepChar(TocEntry[] tracks, String sep) {
391 for(int i = 0; i < tracks.length; i++) {
392 String str = tracks[i].title;
393 if (str.indexOf("Data") != -1) continue;
394 if (str.indexOf(sep) == -1) return false;
395 }
396 return true;
397 }
398 private String idStr, urlString;
399 // part of an attempt to read some broken entries
400 private String stuff;
401 private File cacheDir;
402 private String logOutput;
403 // proxy stuff
404 private boolean usingProxy;
405 private String proxyHost;
406 private int proxyPort;
407 private String savedProxySet, savedProxyHost, savedProxyPort;
408 private Properties systemProperties;
409 }
410
411 /*
412 Local Variables:
413 mode:java
414 indent-tabs-mode:nil
415 c-basic-offset:4
416 c-indent-level:4
417 c-continued-statement-offset:4
418 c-brace-offset:-4
419 c-brace-imaginary-offset:-4
420 c-argdecl-indent:0
421 c-label-offset:0
422 End:
423 */