/* cdown.c version 0.6
 *
 * utility to contact a CDDB server and download information about the
 * currently loaded CD in a format suitable for use with the cdtools
 * package, and specifically cdir.
 *
 * Copyright (C) 1998 Byron Ellacott
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Any modifications in distributed source should be clearly stated
 * immediately following this comment block.
 * 
 */

/* CHANGELOG
 *
 * 0.6          Modifications by Max Vozeler
 *              Relicensed under the GNU GPL with Byron's permission.
 *
 *    Subject: Re: cdown.c
 *    From: Byron Ellacott <bje@apnic.net>
 *    To: Max Vozeler <max@hinterhof.net>
 *    Date: Mon, 11 Oct 2004 08:42:26 +1000
 *    Message-ID: <4169BAD2.8010307@apnic.net>
 *  
 *    Max Vozeler wrote:
 *    >This is probably tangential to the above question, but I was wondering
 *    >if you would consider to relicense cdown.c under the GPL, as that would
 *    >make modifications of cdown and inclusion in other software
 *    >"unambiguously" OK.
 *  
 *    Absolutely.  Please feel free to alter the source to place it under the
 *    terms of the GPL.  The intent of the original license was never to
 *    prevent distribution or modification, so I see no reason not to loosen
 *    the restrictions on those activities.
 *
 * 0.5          Modifications by Peter Samuelson
 *              Added -r option for raw CDDB output
 * 
 * 0.4          Modifications by Max Vozeler
 *              Further adapted to CDTOOL: Include config.h
 *              and use device detection code and constants
 *              from util.h
 *
 * 0.3		Modifications by Max Vozeler
 *		Replace vasprintf() with vsnprintf()
 *		Minor fixes
 *		
 * 0.2		Modifications by Wade Hampton
 *		Environment checking for CDDBHOST
 *		Added verbosity
 *		Added -l option to list available hosts
 *              Some documentation and inclusion in cdtools package
 *
 * 0.1a		Initial release
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <errno.h>
#include <netdb.h>
#include <unistd.h>
#include <string.h>
#include <pwd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#include "config.h"
#include "cdtool.h"
#include "util.h"

#ifdef HAVE_FCNTL_H
# include <fcntl.h>
#endif

/* version */
#define VERSIONSTRING "0.6"

/* default query host and port:  */
#define CDDB_HOST "freedb.freedb.org"
#define CDDB_PORT 888

/* default CD-ROM:  */
#ifndef CD_DEVICE
#define CD_DEVICE "/dev/cdrom"
#endif

typedef struct toc {
  int min, sec, frame;
} CDTocType;

CDTocType cdtoc[100];

static int sock;
static int iRawMode = FALSE;

char *svnid="$Id: cdown.c 1533 2005-10-17 19:11:59Z max $";

/************************************************************************/
/* Procedure:  read_toc
 * Purpose:    to read the CD-ROM's TOC
 * 
 * Inputs:     cd name
 * Outputs:    to TOC structure
 * Returns:    -1 or tochdr.cdth_trk1 (total tracks)
 * Notes:  
 *   1.
 */
/************************************************************************/
int read_toc(char *pszCD) 
{
    int drive;
    struct cdrom_tochdr tochdr;
    struct cdrom_tocentry tocentry;
    int i;

    /* check command line args */
    if (pszCD == NULL)
	return (-1);

    /* open the CD-ROM and query it for the TOC */
    drive = open(pszCD, O_RDONLY | O_NONBLOCK);
    if (drive < 0)
	return (-2);

    if (ioctl(drive, CDROMREADTOCHDR, &tochdr) == -1)
	return (-3);
  
    /* parse the TOC and fill int the cddb TOC table */
    for (i = tochdr.cdth_trk0; i <= tochdr.cdth_trk1; i++) {
	tocentry.cdte_track = i;
	tocentry.cdte_format = CDROM_MSF;
	ioctl(drive, CDROMREADTOCENTRY, &tocentry);
	cdtoc[i-1].min = tocentry.cdte_addr.msf.minute;
	cdtoc[i-1].sec = tocentry.cdte_addr.msf.second;
	cdtoc[i-1].frame = tocentry.cdte_addr.msf.frame;
	cdtoc[i-1].frame += cdtoc[i-1].min*60*75;
	cdtoc[i-1].frame += cdtoc[i-1].sec*75;
    }
    tocentry.cdte_track = 0xAA;
    tocentry.cdte_format = CDROM_MSF;
    ioctl(drive, CDROMREADTOCENTRY, &tocentry);
    cdtoc[tochdr.cdth_trk1].min = tocentry.cdte_addr.msf.minute;
    cdtoc[tochdr.cdth_trk1].sec = tocentry.cdte_addr.msf.second;
    cdtoc[tochdr.cdth_trk1].frame = tocentry.cdte_addr.msf.frame;
    cdtoc[tochdr.cdth_trk1].frame += cdtoc[tochdr.cdth_trk1].min*60*75;
    cdtoc[tochdr.cdth_trk1].frame += cdtoc[tochdr.cdth_trk1].sec*75;
    close(drive);

    return tochdr.cdth_trk1;
}

/************************************************************************/
/* Procedure:  cddb_sum
 * Purpose:    Checksum generation routine used by cddb_discid
 * 
 * Inputs:     n is the offset of the track to checksum, in seconds
 * Outputs:    
 * Returns:    the checksum value for n
 * Notes:  
 *   1. Algorithm and code from http://www.cddb.com/
 */
/************************************************************************/
unsigned int cddb_sum(int n) {
  unsigned int ret;

  ret = 0;
  while (n > 0) {
    ret += (n % 10);
    n /= 10;
  }
  return ret;
}

/************************************************************************/
/* Procedure:  cddb_discid
 * Purpose:    Generates the CDDB disc ID
 * 
 * Inputs:     tot_trks
 * Outputs:    
 * Returns:    The disc ID, as a 32 bit number
 * Notes:  
 *   1. Algorithm and code from http://www.cddb.com/
 */
/************************************************************************/
unsigned long cddb_discid(int tot_trks) {
  unsigned int i, t = 0, n = 0;

  i = 0;
  while (i < tot_trks) {
    n = n + cddb_sum((cdtoc[i].min * 60) + cdtoc[i].sec);
    i++;
  }
  t = ((cdtoc[tot_trks].min * 60) + cdtoc[tot_trks].sec) -
      ((cdtoc[0].min * 60) + cdtoc[0].sec);
  return ((n % 0xff) << 24 | t << 8 | tot_trks);
}

/************************************************************************/
/* Procedure:  netwrite
 * Purpose:    Completely write a buffer to a socket
 * 
 * Inputs:     sock - the socket
 *	       text - data to send
 *	       nchars - length of data to send
 * Outputs:    to sock
 * Returns:    -1 on error, 0 on success
 * Notes:  
 *   1.
 */
/************************************************************************/
int netwrite(int sock, char *text, int nchars) {
  char *strLeft;
  int len;

  strLeft = text;
  do {
    len = write(sock, strLeft, nchars);         /* write as much as we can */
    if (len < 0) return -1;                     /* socket error, damn */
    strLeft += len;                             /* move along the string */
    nchars -= len;                              /* fewer characters to send */
  } while (nchars > 0);                         /* until done entire string */
  return 0;
}

/************************************************************************/
/* Procedure:  netprintf
 * Purpose:    printf() that outputs using netwrite()
 * 
 * Inputs:     sock - the socket to write to
 *	       fmt - format arg, passed to vsnprintf()
 *             ... - other arguments to vsnprintf()
 * Outputs:    
 * Returns:    -1 on error, 0 on success
 * Notes:  
 *   1.
 */
/************************************************************************/
int netprintf(int sock, char *fmt, ...) {
  char buffer[8192];
  va_list args;
  size_t len;

  va_start(args, fmt);
  len = vsnprintf(buffer, sizeof(buffer), fmt, args);
  va_end(args);

  if (len < 1 || len > sizeof(buffer)) {
      fprintf(stderr, "%s len=%d in netprintf\n", (len > sizeof(buffer)) ?
        "truncated message" : "empty message or error", len);
      return -1;
  }

  return netwrite(sock, buffer, len);
}

/************************************************************************/
/* Procedure:  netconnect
 * Purpose:    attempt to open a TCP connection to hostname, on port
 *             port
 * 
 * Inputs:     progname - used in error reporting
 *             hostname - host to connect to
 *             port - port number to connect to
 * Outputs:    to stderr
 * Returns:    the newly created socket
 * Notes:  
 *   1. calls exit(3) on error
 */
/************************************************************************/
int netconnect(char *hostname, int port, int iVerbose) {
  int sock;
  struct hostent *host;
  struct sockaddr_in iaddr;

  if (iVerbose)
      fprintf(stderr, "Resolving host %s\n", hostname);

  /* resolve the host name */
  host = gethostbyname(hostname);
  if (host == NULL) {
      errormsg("error: Getting host info for %s", hostname);
      errormsg("%s", strerror(errno));
      return -1;
  }

  iaddr.sin_port = htons(port);
  iaddr.sin_addr.s_addr = *(unsigned int *)host->h_addr;
  iaddr.sin_family = AF_INET;

  /* create the socket */
  if ((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
      fprintf (stderr, "error: Creating socket\n");
      perror("        error");
      return -1;
  }

  if (iVerbose) fprintf(stderr, "Connecting...\n");

  /* make the connection */
  if (connect(sock, (struct sockaddr *)&iaddr, sizeof(iaddr)) == -1) {
      fprintf (stderr, "error: Connecting to host %s port %d\n", 
	       hostname, port);
      perror("error");
      close(sock);
      return -1;
  }

  return sock;
}

/************************************************************************/
/* Procedure:  getline
 * Purpose:    read a line of text from the CDDB server
 * 
 * Inputs:     
 * Outputs:    
 * Returns:    The line of text
 * Notes:  
 *   1. The CDDB databases use \r\n to end lines.  All lines returned
 *      by getline() have the \r and \n stripped.
 *   2. Non-reentrant: the memory buffer returned previously is
 *      recycled
 *   3. Do NOT modify the contents of the returned buffer past the
 *      NULL - data from the network is stored there
 */
/************************************************************************/
char *getline(void) {
  static char *buffer = NULL;
  static char *lastline = NULL;
  static int buffered = 0;
  char newdata[1024];
  char *nl = NULL;
  int len;

  /* Copy the previously buffered data and resize the buffer */
  if (lastline != NULL) {
    memmove(lastline, buffer, buffered+1);
    buffer = realloc(lastline, buffered+1);
  }

  /* while an end of line marker isn't reached */
  while (buffer == NULL || (nl = strchr(buffer, '\n')) == NULL) {
    if ((len = read(sock, newdata, 1024)) < 0) {
        fprintf(stderr, "error: reading from socket: %s\n", strerror(errno));
        return NULL;
    }
    buffer = realloc(buffer, buffered+len+1);	/* grow the buffer */
    memcpy(buffer+buffered, newdata, len);	/* add the new data */
    buffer[buffered+len] = '\0';		/* make it asciiz */
    buffered += len;
  }

  lastline = buffer;			/* remember the buffer */
  buffer = nl+1;			/* this is the unused data */
  *nl = '\0';				/* EOL marking */
  buffered -= strlen(lastline)+1;	/* this data is no longer buffered */
  nl -= 1;				/* adjust for the NULL */
  if (*nl == 0x0d) *nl = '\0';		/* strip any \r's */
  return lastline;			/* return the data */
}

/************************************************************************/
/* Procedure:  signon
 * Purpose:    parse the initial connection message
 * 
 * Inputs:     message - the response from the server
 * Outputs:    to stderr
 * Returns:    1 on successful connect, 0 on failed connect
 * Notes:  
 *   1. Displays an error message to stdout in the event of an error
 */
/************************************************************************/
int signon(char *message) {
  switch (atoi(message)) {
    case 200: return 1;
    case 201: return 1;
    case 432: fprintf(stderr, "error: permission denied\n"); return 0;
    case 433: fprintf(stderr, "error: server is full\n"); return 0;
    case 434: fprintf(stderr, "error: server load is too high\n"); return 0;
    default: fprintf(stderr, "error: bad message: %s\n", message); return 0;
  }
}

/************************************************************************/
/* Procedure:  welcome
 * Purpose:    parse the server response to ``cddb hello ...''
 * 
 * Inputs:     message - the response from the server
 * Outputs:    to stderr
 * Returns:    1 on success, 0 on failure
 * Notes:  
 *   1. Should never get a 402, check present just in case
 */
/************************************************************************/
int welcome(char *message) {
  int status = 1;

  switch (atoi(message)) {
    case 431: fprintf(stderr, "error: handshake failed\n"); status = 0; break;
    case 402: fprintf(stderr, "warning: already shook hands?\n"); break;
  }
  return status;
}

/************************************************************************/
/* Procedure:  printcd
 * Purpose:    Tidily displays the title of a CD
 * 
 * Inputs:     message - the CD title as returned by CDDB
 * Outputs:    to stderr
 * Returns:    
 * Notes:  
 *   1. Seems neat enough for the time being
 */
/************************************************************************/
void printcd(char *message) {
  fprintf(stderr, "%s\n", message);
}

/************************************************************************/
/* Procedure:  readinfo
 * Purpose:    download and output the track information for a CD
 * 
 * Inputs:     categ, discid - the identifiers used by CDDB
 * Outputs:    to stderr and stdout
 * Returns:    nothing
 * Notes:  
 *   1. Nice to make this append to .cdtooldb when it's close enough
 *      to bug free
 */
/************************************************************************/
void readinfo(char *categ, char *discid) {
  char *message;
  char *firstword;
  char *cdtitle;

  netprintf(sock, "cddb read %s %s\n", categ, discid);
  message = getline();
  switch (atoi(message)) {
    case 401: fprintf(stderr, "error: entry not found\n"); return;
    case 402: fprintf(stderr, "error: server error\n"); return;
    case 403: fprintf(stderr, "error: database corrupt\n"); return;
    case 409: fprintf(stderr, "error: no handshake\n"); return;
    case 210: break;
    default: fprintf(stderr, "error: bad message: %s\n", message); return;
  }
  while (iRawMode) {	/* infinite loop, or skip if !iRawMode */
    message = getline();
    if (!strcmp (message, "."))
      return;
    printf ("%s\r\n", message);
  }
  do {
    message = getline();
    firstword = strchr(message, '=');
    if (firstword != NULL) {
      *firstword = '\0';
      if (strcmp(message, "DTITLE") == 0) {
        cdtitle = strchr(firstword+1, '/');
	if (cdtitle == NULL) {
	  cdtitle = firstword+1;
	} else {
	  *cdtitle = ' ';
	  while(*--cdtitle == ' ') ;
	  *++cdtitle = '\0';
	  while (*++cdtitle == ' ') ;
	}
        printf("cdname %s\n", cdtitle);
	printf("artist %s\n", firstword+1);
      } else if (strncmp(message, "TTITLE", 6) == 0) {
        printf("track %s\n", firstword+1);
      }
    }
/*    printf("%s\n", message);*/
  } while (strcmp(message, ".") != 0);
  printf("\n");
}

/************************************************************************/
/* Procedure:  query
 * Purpose:    ask cddb server about CDs matching the generated discid
 * 
 * Inputs:     
 * Outputs:    to stderr
 * Returns:    
 * Notes:  
 *   1. queries user if discid has multiple matches
 */
/************************************************************************/
void query(void) {
  char *message;
  char *categ, *discid;
  char **matches = NULL;
  int count = 0;
  int input = 0;
  int exact = 1;

  message = getline();
  switch (atoi(message)) {
    case 403: fprintf(stderr, "error: database entry is corrupt\n"); return;
    case 409: fprintf(stderr, "error: no handshake\n"); return;
    case 202: fprintf(stderr, "error: no match\n"); return;
    case 200: message += 4; break;
    case 211: exact = 0; break;
    default: fprintf(stderr, "error: bad message: %s\n", message); return;
  }
  if (!exact) {
    fprintf(stderr, "CD has no exact match\n 0: None\n");
    do {
      message = getline();
      if (strcmp(message, ".") != 0) {
        matches = realloc(matches, sizeof(char*) * (++count));
        fprintf(stderr, " %d: ", count); printcd(message);
        matches[count-1] = malloc(strlen(message)+1);
        strcpy(matches[count-1], message);
      }
    } while (strcmp(message, ".") != 0);
    fprintf(stderr, "Enter the number corresponding to your CD? ");
    scanf("%d", &input);	/* could be done better? */
    input--;
    if (input == -1) return;
    message = matches[input];
  }
  categ = message;
  discid = strchr(categ, ' ');
  *discid++ = '\0';
  message = strchr(discid, ' ');
  *message++ = '\0';
  fprintf(stderr, "Reading information for %s\n", message);
  readinfo(categ, discid);
}

/************************************************************************/
/* Procedure:  PrintHelp
 * Purpose:    print help 
 * 
 * Inputs:     prog name
 * Outputs:    to stderr
 * Returns:    none
 * Notes:  
 *   1.
 */
/************************************************************************/
void PrintHelp(char *pszName)
{
    fprintf (stderr, "%s:  get CD info from cddb database\n", pszName);
    fprintf (stderr, "usage:  cddb [-{1..9}] [-d D] [-H H] [-P P] [-v] [-V|-h|-?]\n");
    fprintf (stderr, "  cddb         get database for default CDROM\n");
    fprintf (stderr, " -d device     specify the CDROM device\n");
    fprintf (stderr, " -H host       get database using HOST (def=%s)\n",
	     CDDB_HOST);
    fprintf (stderr, " -P port       get database using PORT (def=%d)\n",
	     CDDB_PORT);
    fprintf (stderr, " -l            list available cddb hosts\n");
    fprintf (stderr, " -N, N={1..9}  specify /dev/cdromN (def=%s)\n",
	     CD_DEVICE);
    fprintf (stderr, " -r            output raw CDDB entry\n");
    fprintf (stderr, " -v            verbose output\n");
    fprintf (stderr, " -D            enable debugging information\n");
    fprintf (stderr, " -V            print version, exit\n");
    fprintf (stderr, " -?|-h         print help, exit\n\n");
}

/************************************************************************/
/* Procedure:  getuname
 * Purpose:    get the name of the user who ran the program
 * 
 * Inputs:     
 * Outputs:    
 * Returns:    buffer containing username
 * Notes:  
 *   1.
 */
/************************************************************************/
char *getuname(void) {
  struct passwd *pwd;

  pwd = getpwuid(getuid());
  return pwd->pw_name;
}

/************************************************************************/
/* Procedure:  gethname
 * Purpose:    get the name of the host computer
 * 
 * Inputs:     
 * Outputs:    
 * Returns:    buffer containing hostname
 * Notes:  
 *   1.
 */
/************************************************************************/
char *gethname(void) {
  static char buffer[80];	/* is 80 long enough? */

  /* assuming this works is a bad idea ..? */
  gethostname(buffer, sizeof(buffer));
  return buffer;
}

/************************************************************************/
/* Procedure:  parsesites
 * Purpose:    read the list of sites from the server
 * 
 * Inputs:     command line args
 * Outputs:    to stdout (!)
 * Returns:    
 * Notes:  
 *   1.
 */
/************************************************************************/
void parsesites(void) {
  char *buffer;

  buffer = getline();
  switch (atoi(buffer)) {
    case 210: break;		/* OK */
    case 401: fprintf(stderr, "error: no site list available\n"); return;
  }
  buffer = getline();
  do {
    printf("%s\n", buffer);
  } while (strcmp(buffer = getline(), ".") != 0);
}

/************************************************************************/
/* Procedure:  main
 * Purpose:    C code main function
 * 
 * Inputs:     command line args
 * Outputs:    to stdout and stderr
 * Returns:    error code (0 if exit is OK)
 * Notes:  
 *   1.
 */
/************************************************************************/
int main(int argc, char *argv[]) {

  extern int	optind, getopt();
  extern char	*optarg;
  int		iListMode   = FALSE;
  int           error       = 0;
  char          *pszHost    = NULL;
  char          *progname   = NULL;
  int           iPort       = CDDB_PORT;

  int           opt;

  unsigned long discid;
  int tracks, i;
  char *bleh;
  char *device = NULL;
  
  progname = setprogname("cdown");
  cdtool_show_debug = FALSE;
  cdtool_show_verbose = FALSE;
  cdtool_show_crlf = DOLF;
  
  /* parse environment and command line args */
  if ((pszHost = getenv("CDDBHOST")) == NULL)
    pszHost = strdup(CDDB_HOST);

  while ((opt = getopt(argc,argv,"0123456789?DhlrVvH:P:d:")) != EOF) {
      switch (opt) {    
      case '0':
      case '1':
      case '2':
      case '3':
      case '4':
      case '5':
      case '6':
      case '7':
      case '8':
      case '9':
	  device = strdup(CD_DEVICE "?");
	  *(device + sizeof(CD_DEVICE)-1) = opt;
	  break;

      case 'd':
	  device = optarg;
	  break;
	  
      case 'H':
	  pszHost = optarg;
	  break;

      case 'P':
	  iPort = atoi(optarg);
	  break;

      case 'l':
      	  iListMode = TRUE;
	  break;

      case 'r':
	  iRawMode = TRUE;
	  break;

      case 'v':
	  cdtool_show_verbose = TRUE;
	  break;

      case 'D':
	  cdtool_show_debug = TRUE;
	  break;

      case 'V':
	  printf ("%s %s\n", progname, VERSIONSTRING);
	  printf ("Build %s %s [%s]\n",__DATE__,__TIME__, svnid);
          exit(0);
	  
	  /* general options */
      case '?':
      case 'h':
      default:
	  error = 1;
	  break;
      }
  }

  /* if error, print help and exit */
  if (error == 1) {
      PrintHelp(progname);
      exit(1);
  }

  /* try to find the CDROM */
  if ((device = device_detect(device)) == NULL) {
      errormsg("%s: no cdrom found. Try option -d <device>", progname);
      PrintHelp(progname);
      exit(1);
  }

  debugmsg("connecting to host=%s port=%d", pszHost, iPort);
  
  /* connect to the host, if we fail, print error and exit */
  if ((sock = netconnect(pszHost, iPort, cdtool_show_verbose)) < 0) {
      /* error message has already been printed */
      exit(3);
  }

  bleh = getline();
  if (!signon(bleh)) { 
      errormsg("%s:  signon error on host %s", progname, pszHost);
      close(sock); 
      exit(3); 
  }
  
  if (cdtool_show_verbose)
      fprintf(stderr, "Logging in\n");

  debugmsg("sending to CDDB server >> cddb hello %s %s %s %s", 
    getuname(), gethname(), progname, VERSIONSTRING);

  /* send connect string */
  netprintf(sock, "cddb hello %s %s %s %s\n",
    getuname(), gethname(), progname, VERSIONSTRING);
  
  /* get welcome message */
  bleh = getline();
  if (!welcome(bleh)) { 
      errormsg("%s: bad welcome message [%s]", progname, bleh);
      close(sock); 
      exit(3); 
  }

  if (cdtool_show_verbose)
      fprintf(stderr, "Querying\n");

  /* Listing available hosts or querying database? */
  if (iListMode) {
    netprintf(sock, "sites\n");
    parsesites();
  } else {
    /* open CD, read tracks, if fail, report error and exit */
    tracks = read_toc(device);
    if (tracks < 0) {
        errormsg("%s:  error reading cdrom %s", progname, device);
        exit(-1);
    }

    /* generate discid */
    discid = cddb_discid(tracks);

    /* do the query */
    netprintf(sock, "cddb query %08x %d", discid, tracks);

    /* TODO don't print this if there are no matches, or if None is
     * selected */
    if (!iRawMode)
        printf("tracks %d", tracks);

    for (i = 0; i < tracks; i++) {
        netprintf(sock, " %d", cdtoc[i].frame);
        if (!iRawMode)
            printf(" %d", cdtoc[i].frame);
    }

    netprintf(sock, " %d\n", (cdtoc[tracks].frame-cdtoc[0].frame)/75);
    if (!iRawMode)
        printf(" %d\n", (cdtoc[tracks].frame)/75);

    query();
    netprintf(sock, "quit\n");
  }

  exit(0);
}
