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