/* You may freely copy, distribute, modify and use this class as long as the original author attribution remains intact. See message below. Copyright (C) 2003 Christian Pesch. All Rights Reserved. */ /* * --------------------------------------------------------- * Antelmann.com Java Framework by Holger Antelmann * Copyright (c) 2002 Holger Antelmann <info@antelmann.com> * For details, see also http://www.antelmann.com/developer/ * --------------------------------------------------------- */ package slash.metamusic.freedb; import slash.metamusic.discid.DiscId; import slash.metamusic.util.OperationSystem; import slash.metamusic.util.URLLoader; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.net.InetAddress; import java.net.URL; import java.net.URLEncoder; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; import java.util.logging.Logger; /** * FreeDBClient implements the connection to a freedb.org server * or one of its mirrors. <p> * The freedb.org server fully supports the CDDB protocol. * * @author Christian Pesch based on work from Holger Antelmann * @version $Id: FreeDBClient.java 959 2007-03-11 08:21:11Z cpesch $ * @see #setServer(FreeDBServer) */ public class FreeDBClient { /** * Logging output */ protected static final Logger log = Logger.getLogger(FreeDBClient.class.getName()); private static final String LINEBREAK = "\r\n"; private static final String DEFAULT_USER = "unknown"; private static final String CLIENT = "slash.metamusic.freedb.FreeDBClient"; private static final String VERSION = "1.0"; private String user; private FreeDBServer server; private static final String COMMAND = "@command@"; private static final String COMMAND_CAT = "cddb lscat"; private static final String COMMAND_DISCID = "cd "; private static final String COMMAND_MSG = "motd"; private static final String COMMAND_QUERY = "cddb query "; private static final String COMMAND_READ = "cddb read "; private static final String COMMAND_SITES = "sites"; private FreeDBCache freeDBCache = new FreeDBCache(); private boolean useCache = true; /** * uses FreeDBServer.DEFAULT_SERVER * * @see FreeDBServer#DEFAULT_SERVER */ public FreeDBClient() { this(FreeDBServer.DEFAULT_SERVER, fetchUser()); } public FreeDBClient(FreeDBServer server, String user) { setUser(user); setServer(server); } static String fetchUser() { String user = System.getProperty("user.name"); if (user == null) user = DEFAULT_USER; if (user.indexOf(" ") > -1) { StringTokenizer st = new StringTokenizer(user, " "); user = st.nextToken(); } if (user.length() < 1) user = DEFAULT_USER; return user; } public String getUser() { return user; } public void setUser(String user) { this.user = user; } public FreeDBServer getServer() { return server; } /** * Changes the site location used to access the service; the protocol * for the server must be <dfn>http</dfn> * * @throws IllegalArgumentException if the server protocol is not <dfn>http</dfn> * @see #getSites() * @see FreeDBServer */ public void setServer(FreeDBServer server) throws IllegalArgumentException { if (!"http".equals(server.getProtocol())) { throw new IllegalArgumentException("given server doesn't support http"); } this.server = server; } private boolean isReachable() { return getServer().isReachable(); } public boolean isUseCache() { return useCache; } public void setUseCache(boolean useCache) { this.useCache = useCache; } protected String getQueryTemplate() throws UnknownHostException { return "http://" + server.getSite() + ":" + server.getPort() + server.getUri() + "?cmd=" + COMMAND + "&hello=" + user + "+" + InetAddress.getLocalHost().getHostName() + "+" + CLIENT + "+" + VERSION + "&" + "proto=5"; } /** * This method is called for every command supported by the interface * and returns the result as it is provided by the service. <p> * Currently, this method is implemented to use the HTTP protocol * with GET. If other protocols were to be supported (i.e. HTTP POST or * Telnet), only this method needs to be changed/overwritten. * * @param command the entire CLIENT command as specified in the CDDB * protocol; example: "<dfn>cddb lscat</dfn>" */ protected String performCommand(String command) throws IOException { String query = getQueryTemplate().replaceFirst(COMMAND, URLEncoder.encode(command, "UTF-8")); URL url = new URL(query); return URLLoader.getContents(url, true); } /** * Returns a 'message of the day' quote from the freedb server */ public String getMessageOfTheDay() throws IOException { return performCommand(COMMAND_MSG); } /** * Returns sites that can be used as a mirror for this service. <p> * * @see #setServer(FreeDBServer) * @see FreeDBServer */ public FreeDBServer[] getSites() throws IOException { String result = performCommand(COMMAND_SITES); try { int code = Integer.parseInt(result.substring(0, 3)); List<FreeDBServer> servers = new ArrayList<FreeDBServer>(); if (code == 210) { //sites avaliable StringTokenizer line = new StringTokenizer(result, LINEBREAK); line.nextToken(); // initial line w/ code String entry = line.nextToken(); do { CDDBArgumentParser t = new CDDBArgumentParser(entry); FreeDBServer server = new FreeDBServer(t.nextArgument(), t.nextArgument(), Integer.parseInt(t.nextArgument()), t.nextArgument(), t.nextArgument(), t.nextArgument(), t.getRemainder()); servers.add(server); entry = line.nextToken(); } while (!entry.equals(".")); } else { // probably a 401 code - no site information available // let's still return an empty array log.severe("no site information available"); } return servers.toArray(new FreeDBServer[servers.size()]); } catch (Exception e) { if (e instanceof IOException) throw (IOException) e; throw new CDDBProtocolException("could not retrieve sites", getQueryTemplate(), COMMAND_SITES, result, e); } } /** * Returns the categories supported by the freedb server */ public String[] getCategories() throws IOException { String result = performCommand(COMMAND_CAT); try { List<String> categories = new ArrayList<String>(); StringTokenizer st = new StringTokenizer(result, LINEBREAK); st.nextToken(); // ignore the first result line while (st.hasMoreTokens()) { String item = st.nextToken(); if (item.equals(".")) break; categories.add(item); } return categories.toArray(new String[categories.size()]); } catch (Exception e) { throw new CDDBProtocolException("could not retrieve categories", getQueryTemplate(), COMMAND_CAT, result, e); } } /** * Tries to verify the disc id embedded in the DiscId object * by querying the service to recalculate the data. * * @return true only if the calculated disc id by the service * matches the id stored in the DiscId object */ public boolean verifyDiscID(DiscId discId) throws IOException { CDDBArgumentParser p = new CDDBArgumentParser(discId.getFreeDBQueryString()); p.nextArgument(); // embedded cd String command = COMMAND_DISCID + p.getRemainder(); String result = performCommand(command); try { int code = Integer.parseInt(result.substring(0, 3)); if (code != 200) return false; p = new CDDBArgumentParser(result); p.nextArgument(); // code p.nextArgument(); // "Disc" p.nextArgument(); // "ID" p.nextArgument(); // "is" return discId.getEncodedDiscId().equals(p.nextArgument()); } catch (Exception e) { if (e instanceof IOException) throw (IOException) e; throw new CDDBProtocolException("could not verify disc id", getQueryTemplate(), command, result, e); } } /** * Return whether a result for the given <code>DiscId</code> is cached. * * @param discId the <code>DiscId</code> to search the cache for * @return true, if a result for the given <code>DiscId</code> is cached * @throws IOException if an error occurs during the search */ public boolean isDiscIdCached(DiscId discId) throws IOException { return freeDBCache.peekResult(discId) != null; } /** * Returns <code>CDDBRecord</code>s for the given <code>DiscId</code>. * * @param discId the <code>DiscId</code> to query FreeDB for * @return an array of <code>CDDBRecord</code>s * @throws IOException if an error occurs during the query */ public CDDBRecord[] queryDiscId(DiscId discId) throws IOException { String command = COMMAND_QUERY + discId.getFreeDBQueryString(); String result = null; if (isUseCache()) { result = freeDBCache.peekResult(discId); if (result != null) log.info("Query for disc id " + discId.getEncodedDiscId() + " is cached"); } if (result == null) { result = performCommand(command); if (result != null) { log.info("Caching query for disc id " + discId.getEncodedDiscId()); freeDBCache.storeResult(discId, result); } } if (result == null) throw new IOException("No result while quering disc id " + discId); try { int code = Integer.parseInt(result.substring(0, 3)); List<CDDBRecord> records = new ArrayList<CDDBRecord>(); if (code == 200) { // found exact match CDDBArgumentParser item = new CDDBArgumentParser(result.substring(3)); CDDBRecord r = new CDDBRecord(discId, item.nextArgument(), item.nextArgument(), item.getRemainder()); records.add(r); } else if ((code == 211) || (code == 210)) { // found inexact matches (211) or multiple excat matches (210) // code 210 is CDDB protocol level 4 StringTokenizer line = new StringTokenizer(result, LINEBREAK); line.nextToken(); // ignore the first line while (line.hasMoreTokens()) { String entry = line.nextToken(); if (entry.equals(".")) break; CDDBArgumentParser item = new CDDBArgumentParser(entry); CDDBRecord r = new CDDBRecord(discId, item.nextArgument(), item.nextArgument(), item.getRemainder(), code == 210); records.add(r); } } else { // nothing of value found; possible codes: // 202 no match found // 403 database entry is corrupt // 409 no handshake log.severe("No information for disc id " + discId.getEncodedDiscId() + " found"); } return records.toArray(new CDDBRecord[records.size()]); } catch (Exception e) { if (e instanceof IOException) throw (IOException) e; throw new CDDBProtocolException("could not query disc id", getQueryTemplate(), command, result, e); } } /** * It is suggested that the given record was obtained through a call * to queryDiscId(), so that the record is known to exist. * * @return a CDDBEntry instance * @throws CDDBProtocolException if the record doesn't exist * @see CDDBEntry */ public CDDBEntry readCDInfo(CDDBRecord record) throws IOException { String command = COMMAND_READ + record.getCategory() + " " + record.getDiscId(); String result = null; if (isUseCache()) { result = freeDBCache.peekResult(record); if (result != null) log.info("Xmcd for disc id " + record.getDiscId() + " is cached"); } if (result == null) { result = performCommand(command); if (result != null) { log.info("Caching Xmcd for disc id " + record.getDiscId()); freeDBCache.storeResult(record, result); } } try { int code = Integer.parseInt(result.substring(0, 3)); if (code == 210) { BufferedReader in = new BufferedReader(new StringReader(result)); in.readLine(); // ignore first line String fileContent = null; while (in.ready()) { if (fileContent == null) { fileContent = ""; } else { fileContent += LINEBREAK; } String line = in.readLine(); if (line.length() > 255) throw new CDDBProtocolException("a line has been found that exceeds 255 characters", getQueryTemplate(), command, result, null); if (line.equals(".")) break; fileContent += line; } return new CDDBEntry(record, fileContent); } else { String msg; switch (code) { case 401: msg = "Specified CDDB entry not found."; break; case 402: msg = "Server error."; break; case 403: msg = "Database entry is corrupt."; break; case 409: msg = "No handshake."; break; default: msg = "Unknown error message"; } throw new RuntimeException(msg); } } catch (Exception e) { if (e instanceof IOException) throw (IOException) e; throw new CDDBProtocolException("could not read cd", getQueryTemplate(), command, result, e); } } public CDDBRecord[] parseDatafile(DiscId discId, String content) throws IOException { List<CDDBRecord> records = new ArrayList<CDDBRecord>(); // found exact match CDDBArgumentParser item = new CDDBArgumentParser(content); CDDBRecord r = new CDDBRecord(discId, item.nextArgument(), item.nextArgument(), item.getRemainder()); records.add(r); return records.toArray(new CDDBRecord[records.size()]); } public static void main(String[] args) throws Exception { FreeDBClient client = new FreeDBClient(); System.out.println("Server: " + client.getServer()); System.out.println("User: " + client.getUser()); if (client.isReachable()) { System.out.println("MOTD: " + client.getMessageOfTheDay()); FreeDBServer[] servers = client.getSites(); for (int i = 0; i < servers.length; i++) { FreeDBServer server = servers[i]; System.out.println(i + ". server: " + server); } String[] categories = client.getCategories(); for (int i = 0; i < categories.length; i++) { String category = categories[i]; System.out.println(i + ". category: " + category); } } String device = args.length == 0 ? OperationSystem.getDefaultDeviceName() : args[0]; DiscId discId = new DiscId(new File(device)); CDDBRecord[] records = client.queryDiscId(discId); for (int i = 0; i < records.length; i++) { CDDBRecord record = records[i]; System.out.println(i + ". record: " + record); CDDBEntry entry = client.readCDInfo(record); System.out.println(i + ". entry: " + entry); } System.exit(0); } }