diff --git a/projects/samskivert/src/java/com/samskivert/net/cddb/CDDB.java b/projects/samskivert/src/java/com/samskivert/net/cddb/CDDB.java index 1bd835bf..d213b8b4 100644 --- a/projects/samskivert/src/java/com/samskivert/net/cddb/CDDB.java +++ b/projects/samskivert/src/java/com/samskivert/net/cddb/CDDB.java @@ -1,8 +1,472 @@ // -// $Id: CDDB.java,v 1.1 2000/10/23 03:34:28 mdb Exp $ +// $Id: CDDB.java,v 1.2 2000/10/23 07:32:12 mdb Exp $ -package samskivert.net.cddb; +package com.samskivert.net.cddb; +import java.io.*; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; +import java.util.ArrayList; + +/** + * The CDDB class provides access to the information provided by servers + * compliant with the + * CDDB + * protocol. + */ public class CDDB { + /** + * The port on which CDDB servers are generally run. + */ + public static final int STANDARD_PORT = 888; + + /** + * The client name and version reported to the CDDB server. + */ + public static final String CLIENT_NAME_AND_VERSION = + "TSP/CDDB_client $Revision: 1.2 $"; + + public class Entry + { + /** The category to which this entry belongs. */ + public String category; + + /** The unique identifier for this entry. */ + public String cdid; + + /** The human readable title of this entry. */ + public String title; + + /** + * Parses values for this entry from the supplied source + * string. The source string should contain an entry description + * as formatted by the CDDB server. + * + * @exception CDDBException Thrown if the entry is not properly + * formatted. + */ + public void parse (String source) + throws CDDBException + { + int sidx1 = source.indexOf(" "); + int sidx2 = source.indexOf(" ", sidx1+1); + + if (sidx1 == -1 || sidx2 == -1) { + throw new CDDBException(499, "Malformed entry '" + + source + "'"); + } + + category = source.substring(0, sidx1); + cdid = source.substring(sidx1+1, sidx2); + title = source.substring(sidx2+1); + } + } + + /** + * Connects this CDDB instance to the CDDB server running on the + * supplied host using the standard port. + * + *

Note well: you must close a CDDB connection once you are + * done with it, otherwise the socket connection will remain open and + * pointlessly consume machine resources. + * + * @param hostname The host to which to connect. + * + * @exception IOException Thrown if a network error occurs attempting + * to connect to the host. + * @exception CDDBException Thrown if an error occurs after + * identifying ourselves to the host. + * + * @return The message supplied with the succesful connection + * response. + * + * @see #STANDARD_PORT + * @see #close + */ + public String connect (String hostname) + throws IOException, CDDBException + { + return connect(hostname, STANDARD_PORT); + } + + /** + * Connects this CDDB instance to the CDDB server running on the + * supplied host using the specified port. + * + *

Note well: you must close a CDDB connection once you are + * done with it, otherwise the socket connection will remain open and + * pointlessly consume machine resources. + * + * @param hostname The host to which to connect. + * @param port The port number on which to connect to the host. + * + * @exception IOException Thrown if a network error occurs attempting + * to connect to the host. + * @exception CDDBException Thrown if an error occurs after + * identifying ourselves to the host. + * + * @return The message supplied with the succesful connection + * response. + * + * @see #close + */ + public String connect (String hostname, int port) + throws IOException, CDDBException + { + // obtain the necessary information we'll need to identify + // ourselves to the CDDB server + String localhost = InetAddress.getLocalHost().getHostName(); + String username = System.getProperty("user.name"); + if (username == null) { + username = "anonymous"; + } + + // establish our socket connection and IO streams + InetAddress addr = InetAddress.getByName(hostname); + _sock = new Socket(addr, port); + _in = new DataInputStream( + new BufferedInputStream(_sock.getInputStream())); + _out = new PrintStream( + new BufferedOutputStream(_sock.getOutputStream())); + + // first read (and discard) the banner string + _in.readLine(); + + // send a hello request + StringBuffer req = new StringBuffer("cddb hello "); + req.append(username).append(" "); + req.append(localhost).append(" "); + req.append(CLIENT_NAME_AND_VERSION); + + Response rsp = request(req.toString()); + + // confirm that the response was a successful one + if (CDDBProtocol.codeFamily(rsp.code) != CDDBProtocol.OK && + rsp.code != 402 /* already shook hands */) { + throw new CDDBException(rsp.code, rsp.message); + } + + return rsp.message; + } + + /** + * Specifies the number of milliseconds that the client should wait + * for a response from the server before aborting. This must be called + * after a successful call to connect, otherwise the timeout + * will not be set. + */ + public void setTimeout (int timeout) + throws SocketException + { + if (_sock != null) { + _sock.setSoTimeout(timeout); + } + } + + /** + * @return true if this CDDB instance is connected to a CDDB server. + */ + public boolean connected () + { + return _sock != null; + } + + /** + * Closes the connection to the CDDB server previously opened with a + * call to connect(). If the connection was not + * previously established, this member function does nothing. + * + * @see #connect + */ + public void close () + throws IOException + { + if (_sock != null) { + _sock.close(); + + // clear out our references + _sock = null; + _in = null; + _out = null; + } + } + + /** + * Issues a query to the CDDB server using the supplied CD identifying + * information. + * + * @param discid The disc identifier (information on how to compute + * the disc ID is available + * + * here. + * @param frameOffsets The frame offset of each track. The length of + * this array is assumed to be the number of tracks on the CD and is + * used in the query. + * @param length The length (in seconds) of the CD. + * + * @return If no entry matches the query, null is returned. Otherwise + * one or more entries is returned that matched the query parameters. + */ + public Entry[] query (String discid, int[] frameOffsets, int length) + throws IOException, CDDBException + { + // sanity check + if (_sock == null) { + throw new CDDBException(500, "Not connected"); + } + + // construct the query parameter + StringBuffer req = new StringBuffer("cddb query "); + req.append(discid).append(" "); + req.append(frameOffsets.length).append(" "); + for (int i = 0; i < frameOffsets.length; i++) { + req.append(frameOffsets[i]).append(" "); + } + req.append(length); + + // make the request + Response rsp = request(req.toString()); + Entry[] entries = null; + + // if this is an exact match, parse the entry and return it + if (rsp.code == 200 /* exact match */) { + entries = new Entry[1]; + entries[0] = new Entry(); + entries[0].parse(rsp.message); + + } else if (rsp.code == 211 /* inexact matches */) { + // read the matches from the server + ArrayList list = new ArrayList(); + String input = _in.readLine(); + System.out.println("...: " + input); + while (!input.equals(CDDBProtocol.TERMINATOR)) { + Entry e = new Entry(); + e.parse(input); + list.add(e); + input = _in.readLine(); + System.out.println("...: " + input); + } + entries = new Entry[list.size()]; + list.toArray(entries); + + } else if (CDDBProtocol.codeFamily(rsp.code) != CDDBProtocol.OK) { + throw new CDDBException(rsp.code, rsp.message); + } + + return entries; + } + + /** + * A detail object contains all of the detailed information about a + * particular CD as retrieved from the CDDB server. + */ + public class Detail + { + /** The unique identifier for this CD. */ + String discid; + + /** The category to which this CD belongs. */ + String category; + + /** The title of this CD. */ + String title; + + /** The track names. */ + String[] trackNames; + + /** The extended data for the CD. */ + String extendedData; + + /** The extended data for each track. */ + String[] extendedTrackData; + } + + /** + * Requests the detail information for a particular disc in a + * particular category from the CDDB server. + * + * @return A detail instance filled with the information for the + * requested CD. + */ + public Detail read (String category, String discid) + throws IOException, CDDBException + { + // sanity check + if (_sock == null) { + throw new CDDBException(500, "Not connected"); + } + + // construct the query + StringBuffer req = new StringBuffer("cddb read "); + req.append(category).append(" "); + req.append(discid); + + // make the request + Response rsp = request(req.toString()); + // anything other than an OK response earns an exception + if (rsp.code != 210 /* OK, entry follows */) { + throw new CDDBException(rsp.code, rsp.message); + } + + Detail detail = new Detail(); + + // parse the category and discid from the response string + int sidx = rsp.message.indexOf(" "); + if (sidx == -1) { + throw new CDDBException(500, "Malformed read response: " + + rsp.message); + } + detail.category = rsp.message.substring(0, sidx); + detail.discid = rsp.message.substring(sidx+1); + + ArrayList tnames = new ArrayList(); + ArrayList texts = new ArrayList(); + + // now parse the contents + String input = _in.readLine(); + for (int lno = 0; !input.equals(CDDBProtocol.TERMINATOR); lno++) { + + if (input.startsWith("#")) { + // skip comments + + } else if (input.startsWith("DTITLE")) { + if (detail.title == null) { + detail.title = contents(input, lno); + } else { + detail.title += contents(input, lno); + } + + } else if (input.startsWith("EXTD")) { + if (detail.extendedData == null) { + detail.extendedData = contents(input, lno); + } else { + detail.extendedData += contents(input, lno); + } + + } else if (input.startsWith("TTITLE")) { + append(tnames, index(input, "TTITLE", lno), + contents(input, lno)); + + } else if (input.startsWith("EXTT")) { + append(texts, index(input, "EXTT", lno), + contents(input, lno)); + } + + // read in the next line of input + input = _in.readLine(); + } + + // convert the lists into arrays + detail.trackNames = new String[tnames.size()]; + tnames.toArray(detail.trackNames); + detail.extendedTrackData = new String[texts.size()]; + texts.toArray(detail.extendedTrackData); + + return detail; + } + + /** + * Extracts the track index of the supplied line (the number + * immediately following the supplied prefix and preceding the equals + * sign) or throws a CDDBException if the line contains no equals sign + * or track index. + */ + protected final int index (String source, String prefix, int lineno) + throws CDDBException + { + int eidx = source.indexOf("=", prefix.length()); + if (eidx == -1) { + throw new CDDBException(500, "Malformed line '" + source + + "' number " + lineno); + } + + try { + return Integer.parseInt(source.substring(prefix.length(), eidx)); + } catch (NumberFormatException nfe) { + throw new CDDBException(500, "Malformed line '" + source + + "' number " + lineno); + } + } + + /** + * Extracts the contents of the supplied line (everything after the + * equals sign) or throws a CDDBException if the line contains no + * equals sign. + */ + protected final String contents (String source, int lineno) + throws CDDBException + { + int eidx = source.indexOf("="); + if (eidx == -1) { + throw new CDDBException(500, "Malformed line '" + source + + "' number " + lineno); + } + return source.substring(eidx+1); + } + + /** + * Appends the supplied string to the contents of the list at the + * supplied index. If the list has no contents at the supplied + * index, the supplied value becomes the contents at that index. + */ + protected final void append (ArrayList list, int index, String value) + { + // expand the list as necessary + while (list.size() <= index) { + list.add(""); + } + list.set(index, list.get(index) + value); + } + + /** + * A simple class to encapsulate the response from the CDDB server. + */ + protected class Response + { + public int code; + public String message; + } + + /** + * Issues a request to the CDDB server and parses the response. + */ + protected Response request (String req) + throws IOException + { + System.err.println("REQ:" + req); + + // send the request to the server + _out.println(req); + _out.flush(); + + // now read the response + String rspstr = _in.readLine(); + System.err.println("RSP:" + rspstr); + + // parse the response + Response rsp = new Response(); + String codestr = rspstr; + + // assume the response is just a code unless we see a space + int sidx = rspstr.indexOf(" "); + if (sidx != -1) { + codestr = rspstr.substring(0, sidx); + rsp.message = rspstr.substring(sidx+1); + } + + try { + rsp.code = Integer.parseInt(codestr); + } catch (NumberFormatException nfe) { + rsp.code = 599; + rsp.message = "Unparseable response"; + } + + return rsp; + } + + protected Socket _sock; + protected DataInputStream _in; + protected PrintStream _out; } diff --git a/projects/samskivert/src/java/com/samskivert/net/cddb/CDDBException.java b/projects/samskivert/src/java/com/samskivert/net/cddb/CDDBException.java new file mode 100644 index 00000000..b44bb30e --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/net/cddb/CDDBException.java @@ -0,0 +1,28 @@ +// +// $Id: CDDBException.java,v 1.1 2000/10/23 07:32:12 mdb Exp $ + +package com.samskivert.net.cddb; + +/** + * This exception class encapsulates errors that may occur while + * communicating to a CDDB server. It is not used to communicate IO errors + * (an IOException is used for that), but it is used to communicate + * failures communicated within the scope of the CDDB protocol. + * + * @see CDDB + */ +public class CDDBException extends Exception +{ + public CDDBException (int code, String message) + { + super(message); + _code = code; + } + + public int getCode () + { + return _code; + } + + protected int _code; +} diff --git a/projects/samskivert/src/java/com/samskivert/net/cddb/CDDBProtocol.java b/projects/samskivert/src/java/com/samskivert/net/cddb/CDDBProtocol.java new file mode 100644 index 00000000..ba7b1938 --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/net/cddb/CDDBProtocol.java @@ -0,0 +1,51 @@ +// +// $Id: CDDBProtocol.java,v 1.1 2000/10/23 07:32:12 mdb Exp $ + +package com.samskivert.net.cddb; + +/** + * The CDDB protocol class provides constants related to the CDDB protocol + * as well as utility routines useful in dealing with the protocol. + */ +public class CDDBProtocol +{ + /** + * The terminating marker which occurs alone on it's own line to + * indicate the end of an extended command. + */ + public static final String TERMINATOR = "."; + + /* Class constant offsets */ + public static final int INFORMATIONAL = 100; + public static final int OK = 200; + public static final int OK_WITH_CONTINUATION = 300; + public static final int UNABLE_TO_PERFORM = 400; + public static final int SERVER_ERROR = 500; + + /** + * @return The family to which the supplied specific response code + * belongs. + */ + public static int codeFamily (int code) + { + return (code / 100) * 100; + } + + /** + * @return The genus to which the supplied specific response code + * belongs. + */ + public static int codeGenus (int code) + { + return ((code % 100) / 10) * 10; + } + + /** + * @return The species to which the supplied specific response code + * belongs. + */ + public static int codeSpecies (int code) + { + return code % 10; + } +} diff --git a/projects/samskivert/src/java/com/samskivert/net/cddb/CDDBTest.java b/projects/samskivert/src/java/com/samskivert/net/cddb/CDDBTest.java new file mode 100644 index 00000000..6edf5c1e --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/net/cddb/CDDBTest.java @@ -0,0 +1,80 @@ +// +// $Id: CDDBTest.java,v 1.1 2000/10/23 07:32:12 mdb Exp $ + +package com.samskivert.net.cddb; + +/** + * Tests the CDDB code by connecting to a CDDB server and making some + * requests. + */ +public class CDDBTest +{ + public static void test (String hostname) + throws Exception + { + CDDB cddb = new CDDB(); + + try { + String rsp = cddb.connect(hostname); + + // set the timeout to 30 seconds + cddb.setTimeout(30*1000); + + // try a test query + String cdid = "1b037b03"; + int[] offsets = { 150, 18130, 48615 }; + int length = 893; + CDDB.Entry[] entries = cddb.query(cdid, offsets, length); + + if (entries == null || entries.length == 0) { + System.out.println("No match for " + cdid + "."); + + } else { + for (int i = 0; i < entries.length; i++) { + System.out.println("Match " + entries[i].category + "/" + + entries[i].cdid + "/" + + entries[i].title); + } + + CDDB.Detail detail = cddb.read(entries[0].category, + entries[0].cdid); + System.out.println("Title: " + detail.title); + for (int i = 0; i < detail.trackNames.length; i++) { + System.out.println(pad(i) + ": " + detail.trackNames[i]); + } + System.out.println("Extended data: " + detail.extendedData); + for (int i = 0; i < detail.extendedTrackData.length; i++) { + System.out.println(pad(i) + ": " + + detail.extendedTrackData[i]); + } + } + + } finally { + cddb.close(); + } + } + + protected static String pad (int value) + { + return ((value > 9) ? "" : " ") + value; + } + + public static void main (String[] args) + { + if (args.length < 1) { + System.err.println("Usage: CDDBTest cddbhost"); + System.exit(-1); + } + + try { + test(args[0]); + + } catch (CDDBException ce) { + System.err.println("Protocol exception: " + ce.getCode() + + ": " + ce.getMessage()); + + } catch (Throwable t) { + t.printStackTrace(System.err); + } + } +} diff --git a/projects/samskivert/src/java/com/samskivert/net/cddb/Makefile b/projects/samskivert/src/java/com/samskivert/net/cddb/Makefile index 99d13295..e66ec7c0 100644 --- a/projects/samskivert/src/java/com/samskivert/net/cddb/Makefile +++ b/projects/samskivert/src/java/com/samskivert/net/cddb/Makefile @@ -1,8 +1,12 @@ # -# $Id: Makefile,v 1.1 2000/10/23 03:34:28 mdb Exp $ +# $Id: Makefile,v 1.2 2000/10/23 07:32:12 mdb Exp $ -ROOT = ../../.. +ROOT = ../../../.. SRCS = \ + CDDB.java \ + CDDBException.java \ + CDDBProtocol.java \ + CDDBTest.java \ include $(ROOT)/build/Makefile.java