/*
 * $Id$
 *
 * This file is part of WorkMan, the civilized CD player library
 * (c) 1991-1997 by Steven Grimm (original author)
 * (c) by Dirk Försterling (current 'author' = maintainer)
 * The maintainer can be contacted by his e-mail address:
 * milliByte@DeathsDoor.com 
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the Free
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 *
 * Manage the CD database.  All these routines assume that the "cd" global
 * structure contains current information (as far as the outside world knows;
 * obviously it won't contain track titles just after a CD is inserted.)
 */

#define RCFILE "/.workmanrc"
#define DBFILE "/.workmandb"
#define FUZZFRAMES 75

#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/param.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <time.h>
#include "include/wm_config.h"
#include "include/wm_helpers.h"
#include "include/wm_struct.h"
#include "include/wm_cdinfo.h"
#include "include/wm_cddb.h"
#include "include/wm_index.h"
#include "include/wm_database.h"

#define WM_MSG_CLASS WM_MSG_CLASS_DB

#define SWALLOW_LINE(fp) { int _c; while ((_c = getc(fp)) != '\n' && _c != EOF); }

/* local prototypes */
char *print_cdinfo(struct wm_cdinfo *pcd, int prefs);
FILE *open_rcfile(const char *name, const char *mode);
int *reset_tracks(void);
int search_db( FILE *fp, int prefs, int scan, int holesize_wanted );
void spinwheels(int secs);
int lockit(int fd, int type);
void save_globals(FILE *fp);
int save_entry(char *filename, int pref);
/* local prototypes END */

static int	suppress_locking = 0;	/* Turn off locking of datafile (dangerous) */

static char	*rcfile = NULL;		/* Personal rcfile */
static char	*dbfiles = NULL;	/* Colon-separated list of databases */
static char	**databases = NULL;	/* NULL-terminated list of databases */

static char	*otherrc = NULL;	/* Unrecognized cruft from start of rcfile */

static long	rcpos, rclen;		/* XXX */

static int	found_in_db, found_in_rc;
static long	holepos, firstpos;

static int fuzz_frames = FUZZFRAMES;

static int wm_db_save_disabled = FALSE;

static int cur_playnew = -1;

extern int cur_ntracks, cur_nsections;

int mark_a = 0;
int mark_b = 0;


/*
 *
 */
int wm_db_get_playnew( void )
{
	return 0;
}

/*
 * split_workmandb()
 *
 * Split the WORKMANDB environment variable, if any, into a list of database
 * files in the global "databases".  If WORKMANDB is not available, make
 * a single entry with $HOME/DBFILE.
 *
 * Also, fill the "rcfile" global with the personal preferences filename.
 *
 * The environment variables should have already been read and placed in the
 * "rcfile" and "dbfiles" globals, respectively.
 */
void
split_workmandb( void )
{
	int	ndbs, i;
	char	*home, *wmdb;
	int	no_rc = 0, no_db = 0;
	
	if (rcfile == NULL)
	{
		if ((home = getenv("HOME")) != NULL)
		{
			rcfile = malloc(strlen(home) + sizeof(RCFILE));
			if (rcfile == NULL)
			{
			
nomem:
				perror("split_workmandb()");
				exit(1);
			}

			strcpy(rcfile, home);
			strcat(rcfile, RCFILE);
		}
		else
			no_rc = 1;

	}

	if ((wmdb = dbfiles) == NULL)
	{
		if ((home = getenv("HOME")) != NULL)
		{
			wmdb = malloc(strlen(home) + sizeof(DBFILE));
			if (wmdb == NULL)
				goto nomem;

			databases = malloc(2 * sizeof (databases[0]));
			if (databases == NULL)
				goto nomem;

			strcpy(wmdb, home);
			strcat(wmdb, DBFILE);
			databases[0] = wmdb;
			databases[1] = NULL;
		}
		else
		{
			static char *emptydb = NULL;
		
			databases = &emptydb;
			no_db = 1;
		}
	}
	else
	{
		ndbs = 1;
		for (home = wmdb; *home; home++)
			if (*home == ':')
			{
				*home = '\0';
				ndbs++;
			}
		
		databases = malloc((ndbs + 1) * sizeof(databases[0]));
		if (databases == NULL)
			goto nomem;
		
		for (i = 0; i < ndbs; i++)
		{
			databases[i] = wmdb;
			wmdb += strlen(wmdb) + 1;
		}

		databases[i] = NULL;
	}

	if (no_db || no_rc)
	{
#ifndef NDEBUG
		fprintf(stderr,
"WorkMan was run without a home directory, probably by a system daemon.\n");
		fprintf(stderr, "It doesn't know where to find ");
		if (no_rc)
		{
			fprintf(stderr, "your personal preferences file ");
			if (no_db)
				fprintf(stderr, "or the\ndatabase of CD descriptions");
		}
		else
			fprintf(stderr, "the database of CD descriptions");

		fprintf(stderr,
".\nYou can use the X resources \"workman.db.shared\" and \"workman.db.personal\"\nto tell WorkMan where to look.\n");
#endif
		wm_db_save_disabled = TRUE;
	}
}

/*
 * print_cdinfo(cd, prefs)
 *
 * cd		A pointer to a cdinfo struct.
 * prefs	Flag: write personal preferences?
 *
 * Print a CD's information (in more or less readable form) to a buffer.
 * Returns a pointer to the buffer.
 *
 * XXX - could be more efficient about calling wm_strmcat() and strlen().
 */
char *
print_cdinfo(struct wm_cdinfo *cd, int prefs)
{
	int		i;
	char		tempbuf[2000];	/* XXX - is this always big enough? */
	static char	*cdibuf = NULL;
	struct wm_playlist	*l;

	sprintf(tempbuf, "\ntracks %d", cd->ntracks);
	for (i = 0; i < cur_ntracks; i++)
		if (cd->trk[i].section < 2)
			sprintf(tempbuf + strlen(tempbuf), " %d",
				cd->trk[i].start);
	sprintf(tempbuf + strlen(tempbuf), " %d\n", cd->length);

	wm_strmcpy(&cdibuf, tempbuf);

	if (cur_nsections)
	{
		sprintf(tempbuf, "sections %d", cur_nsections);
		/* fixed a bug here */
		for (i = 0; i < cur_ntracks; i++)
			if (cd->trk[i].section > 1)
				sprintf(tempbuf + strlen(tempbuf), " %d",
					cd->trk[i].start);
		sprintf(tempbuf + strlen(tempbuf), "\n");

		wm_strmcat(&cdibuf, tempbuf);
	}

	if (prefs)
	{
		if (cd->autoplay)
			wm_strmcat(&cdibuf, "autoplay\n");
		for (l = cd->lists; l != NULL && l->name != NULL; l++)
		{
			wm_strmcat(&cdibuf, "playlist ");

			i = strlen(cdibuf) - 1;
			wm_strmcat(&cdibuf, l->name);
			while (cdibuf[++i])
				if (cdibuf[i] == ' ' || cdibuf[i] == '\t')
					cdibuf[i] = '_';

			if (l->list != NULL)
			{
				for (i = 0; l->list[i]; i++)
					;
				sprintf(tempbuf, " %d", i);
				wm_strmcat(&cdibuf, tempbuf);
				for (i = 0; l->list[i]; i++)
				{
					sprintf(tempbuf, " %d", l->list[i]);
					wm_strmcat(&cdibuf, tempbuf);
				}
				wm_strmcat(&cdibuf, "\n");
			}
			else
				wm_strmcat(&cdibuf, " 0\n");
		}

		if (cd->volume)
		{
			/*
			 * Have to maintain compatibility with old versions,
			 * where volume was 0-32.
			 */
			sprintf(tempbuf, "cdvolume %d\n", (cd->volume * 32) / 100);
			wm_strmcat(&cdibuf, tempbuf);
		}

		if (cd->playmode)
		{
			sprintf(tempbuf, "playmode %d\n", cd->playmode);
			wm_strmcat(&cdibuf, tempbuf);
		}

		if (mark_a)
		{
			sprintf(tempbuf, "mark %d START\n", mark_a);
			wm_strmcat(&cdibuf, tempbuf);
		}
		if (mark_b)
		{
			sprintf(tempbuf, "mark %d END\n", mark_b);
			wm_strmcat(&cdibuf, tempbuf);
		}

		if (cd->otherrc)
			wm_strmcat(&cdibuf, cd->otherrc);

		for (i = 0; i < cur_ntracks; i++)
		{
			if (cd->trk[i].avoid)
			{
				sprintf(tempbuf, "dontplay %d\n", i + 1);
				wm_strmcat(&cdibuf, tempbuf);
			}
			if (cd->trk[i].volume)
			{
				sprintf(tempbuf, "volume %d %d\n", i + 1,
					(cd->trk[i].volume * 32) / 100);
				wm_strmcat(&cdibuf, tempbuf);
			}
			if (cd->trk[i].otherrc)
				wm_strmcat(&cdibuf, cd->trk[i].otherrc);
		}
	}
	else
	{
		if (cd->cdname[0])
		{
			wm_strmcat(&cdibuf, "cdname ");
			wm_strmcat(&cdibuf, cd->cdname);
			wm_strmcat(&cdibuf, "\n");
		}

		if (cd->artist[0])
		{
			wm_strmcat(&cdibuf, "artist ");
			wm_strmcat(&cdibuf, cd->artist);
			wm_strmcat(&cdibuf, "\n");
		}

		if (cd->otherdb)
			wm_strmcat(&cdibuf, cd->otherdb);

		for (i = 0; i < cur_ntracks; i++)
		{
			if (cd->trk[i].section > 1)
				wm_strmcat(&cdibuf, "s-");
			wm_strmcat(&cdibuf, "track ");
			if (cd->trk[i].songname != NULL)
				wm_strmcat(&cdibuf, cd->trk[i].songname);
			wm_strmcat(&cdibuf, "\n");
			if (cd->trk[i].contd)
			{
				if (cd->trk[i].section > 1)
					wm_strmcat(&cdibuf, "s-");
				wm_strmcat(&cdibuf, "continue\n");
			}
			if (cd->trk[i].otherdb)
				wm_strmcat(&cdibuf, cd->trk[i].otherdb);
		}
	}

	return (cdibuf);
} /* print_cdinfo() */

/*
 * Open the rcfile for reading or writing.
 *
 *	name		Filename
 *	mode		"r" or "w"
 */
FILE *
open_rcfile(const char *name, const char *mode)
{
	FILE		*fp;
	struct stat	st;

	fp = fopen(name, mode);
	if (fp == NULL)
	{
		if (errno != ENOENT || mode[0] == 'w')
			perror(name);
	}
	else
	{
		/* Don't let people open directories or devices */
		if (fstat(fileno(fp), &st) < 0)
		{
			perror(name);
			fclose(fp);
			return (NULL);
		}

#ifdef S_ISREG
		if (! S_ISREG(st.st_mode))
#else
		if ((st.st_mode & S_IFMT) != S_IFREG)
#endif
		{
			errno = EISDIR;
			perror(name);
			fclose(fp);
			return (NULL);
		}

		if (mode[0] == 'w') /* create -- put data in so locks work */
		{
			fputs("# WorkMan database file\n", fp);
			fclose(fp);
			fp = fopen(name, "r+");
			if (fp == NULL)
				if (errno != ENOENT)
					perror(name);
		}
	}

	return (fp);
}

/*
 *
 * allocate and clear "trackmap".
 *
 */
int *reset_tracks(void)
{
    int i, j;
    int *trackmap;
    /*
     * Since we access track numbers indirectly (to handle sections
     * with at least a little elegance), the track mapping needs to be
     * set up before we read anything.  Initially it must assume that
     * no sections will appear in this datafile.
     */
    trackmap = malloc(sizeof(int) * cur_ntracks);
    if (trackmap == NULL)
    {
    	perror("trackmap");
	exit(1);
    }
    j = 0;
    for (i = 0; i < cd->ntracks; i++)
    {
	trackmap[i] = j;
	while (cd->trk[++j].section > 1)
		;
    }
    return trackmap;
} /* reset_tracks() */

/*
 * Load a new-format database file, searching for a match with the currently
 * inserted CD.  Modify the in-core copy of the CD info based on what's found
 * in the database.
 *
 * Returns 1 if there was a match or 0 if not.
 *
 *	fp		FILE* of database or rcfile.
 *	prefs		1 if we're searching .workmanrc, 0 for .workmandb,
 *			2 if just reading settings
 *	scan		Scan for "tracks" location and entry size only
 *	holesize_wanted	How big a hole we're looking for, if any.
 *
 * If a hole was found along the way, update the global "holepos" with its
 * starting offset in the file.  A hole is defined as a bunch of blank lines
 * preceding a "tracks" line.  Holepos will contain the best match.
 *
 * In addition, "firstpos" will be filled with the position of the first
 * "tracks" keyword, so we know how much room is available for global
 * settings at the rcfile's start.
 */
int
search_db( FILE *fp, int prefs, int scan, int holesize_wanted )

{
	char	keyword[64], listname[64], *c;
	int	b, i, j, track = 0, listsize, ntracks, scratch, searching = 1;
	int	*trackmap = 0, gotsections = 0;
        int     fudge, maxfudge, sizediff, bestfudge = 0;
	long	pos = 0, thisholepos = -1, holesize = 99991239;
	struct	wm_playlist *l;

	rclen = 0;

	wm_lib_message(WM_MSG_CLASS_DB|WM_MSG_LEVEL_DEBUG , "db: Reached search_db()\n" );

	/* We may not find any holes at all! */
	if (holesize_wanted)
		holepos = -1;

	if( prefs != 2 )
		trackmap = reset_tracks();

	if (prefs)
		freeup(&otherrc);
	firstpos = -1;
	while (! feof(fp))
	{
		pos = ftell(fp);
		keyword[0] = '\0';
		do
			b = getc(fp);
		while (b != EOF && b != '\n' && isspace(b));

		if (b == EOF || feof(fp))
			break;

		if (b != '\n')
		{
			keyword[0] = b;
			fscanf(fp, "%62s", &keyword[1]);
		}
		if (keyword[0] == '\0')		/* Blank line. */
		{
			if (thisholepos < 0)
				thisholepos = pos;
			continue;
		}

		/* Strip off "s-" if we've seen a "sections" keyword */
		if (gotsections && keyword[0] == 's' && keyword[1] == '-')
		{
			for (c = &keyword[2]; (c[-2] = *c) != '\0'; c++)
				;
			wm_lib_message(WM_MSG_CLASS_DB|WM_MSG_LEVEL_DEBUG , "db: stripped off the 's-'. Result is %s\n", keyword);
		}

		/* If this is the start of a CD entry, see if it matches. */
		if (! strcmp(keyword, "tracks"))
		{
			if (prefs == 2)
				break;

			/* Is this the end of a hole? */
			if (holesize_wanted && (thisholepos >= 0))
			{
				/* Yep.  Is it better than the last one? */
				if (pos - thisholepos < holesize && pos -
						thisholepos >= holesize_wanted)
				{
					holepos = thisholepos;
					holesize = pos - thisholepos;
				}
				thisholepos = -1;
			}

			/* Is it the start of the CD entries? */
			if (firstpos == -1)
				firstpos = pos;

			/* Is this the end of the entry we really wanted? */
			if (! searching)
			{
				rclen = pos - rcpos;
				break;
			}

                        /* If we have a near match, indicate that we
                            should stop reading tracks, etc now */
                        if (searching == 2)
                        {
                            searching = 3;
                            continue;
                        }

			fscanf(fp, "%d", &ntracks);

			if (ntracks != cd->ntracks)
			{
chomp:
				SWALLOW_LINE(fp);
				continue;
			}

                        fudge = 0;
                        maxfudge = (ntracks * fuzz_frames) >> 1;
			track = 0;
			for (i = 0; i < ntracks; i++)
			{
				fscanf(fp, "%d", &scratch);
				if (scratch != cd->trk[track].start)
                                {
                                    sizediff = abs(scratch - cd->trk[track].start);
                                    if (sizediff > fuzz_frames || 
                                        (sizediff && scan))
                                      break;
                                    fudge += sizediff;
                                }
				while (cd->trk[++track].section > 1)
					;
			}
			if (i != ntracks)
				goto chomp;

                        if (fudge > 0) /* best near match? */
                        {
                            if (fudge > maxfudge)
                                goto chomp;
                            if (bestfudge == 0 || fudge < bestfudge)
                                bestfudge = fudge;
                            else
                                goto chomp;
			    rcpos = pos;
			    track = 0;
                            searching = 2;
                        }
                        else /* probably exact match */
                        {
			    fscanf(fp, "%d", &scratch);

			    if (scratch != -1 && scratch != cd->length)
				    goto chomp;

			    /* Found it! */
			    rcpos = pos;
			    track = 0;
			    searching = 0;
                        }

			SWALLOW_LINE(fp);	/* Get rid of newline */
		}

		/* Global mode stuff goes here */
		else if (! strcmp(keyword, "cddbprotocol"))
		{
			getc(fp);
			i = getc(fp);	/* only first letter is used */
		        cddb.protocol = i == 'c' ? 1 : 
		                        i == 'h' ? 2 : 3 ;
			do
				i = getc(fp);
			while (i != '\n' && i != EOF);
		}

		else if (! strcmp(keyword, "cddbserver"))
		{
			getc(fp);	/* lose the space */
			if (cddb.cddb_server[0])
				do
					i = getc(fp);
				while (i != '\n' && i != EOF);
			else
			{
				fgets(cddb.cddb_server, 
				      sizeof(cddb.cddb_server), fp);
				if ((i = strlen(cddb.cddb_server)))
					cddb.cddb_server[i - 1] = '\0';
			}
		}

		else if (! strcmp(keyword, "cddbmailadress"))
		{
			getc(fp);	/* lose the space */
			if (cddb.mail_adress[0])
				do
					i = getc(fp);
				while (i != '\n' && i != EOF);
			else
			{
				fgets(cddb.mail_adress, 
				      sizeof(cddb.mail_adress), fp);
				if ((i = strlen(cddb.mail_adress)))
					cddb.mail_adress[i - 1] = '\0';
			}
		}

		else if (! strcmp(keyword, "cddbpathtocgi"))
		{
			getc(fp);	/* lose the space */
			if (cddb.path_to_cgi[0])
				do
					i = getc(fp);
				while (i != '\n' && i != EOF);
			else
			{
				fgets(cddb.path_to_cgi, 
				      sizeof(cddb.path_to_cgi), fp);
				if ((i = strlen(cddb.path_to_cgi)))
					cddb.path_to_cgi[i - 1] = '\0';
			}
		}

		else if (! strcmp(keyword, "cddbproxy"))
		{
			getc(fp);	/* lose the space */
			if (cddb.proxy_server[0])
				do
					i = getc(fp);
				while (i != '\n' && i != EOF);
			else
			{
				fgets(cddb.proxy_server, 
				      sizeof(cddb.proxy_server), fp);
				if ((i = strlen(cddb.proxy_server)))
					cddb.proxy_server[i - 1] = '\0';
			}
		}

		else if (! strcmp(keyword, "whendone"))
		{
			getc(fp);
			i = getc(fp);	/* only first letter is used */
			if (cur_stopmode == -1)	/* user's setting preferred */
				cur_stopmode = i == 's' ? 0 : i == 'r' ? 1 : 2;
			do
				i = getc(fp);
			while (i != '\n' && i != EOF);
		}

		else if (! strcmp(keyword, "playnew"))
		{
			if (cur_playnew == -1)
				cur_playnew = 1;
			SWALLOW_LINE(fp);
		}

		/* If we're searching, skip to the next "tracks" line. */
		else if (((searching & 1)|| scan) 
                         && !(prefs && firstpos == -1))
			SWALLOW_LINE(fp)

		else if (! strcmp(keyword, "sections"))
		{
			gotsections = 1;
			fscanf(fp, "%d", &ntracks);

			free(trackmap);
			trackmap = (int *) malloc(sizeof(int) *
						(cur_ntracks + ntracks));
			if (trackmap == NULL)
			{
				perror("section mapping");
				exit(1);
			}

			/*
			 * If sections are already defined, use these as a
			 * reference, mapping this CD entry's section numbers
			 * to the ones in core.
			 *
			 * Otherwise, split the CD up according to the sections
			 * listed here.
			 */
			if (cur_nsections)
			{
				track = 0;
				i = 0;
				while (ntracks)
				{
					ntracks--;
					fscanf(fp, "%d", &scratch);
					while (scratch > cd->trk[track].start)
					{
						if (cd->trk[track].section < 2)
							trackmap[i++] = track;
						++track;

						if (track == cur_ntracks)
							break;
					}

					/* rc has later sections than db... */
					if (track == cur_ntracks)
						break;

					/* Matches can be approximate */
					if (scratch+75 > cd->trk[track].start &&
					    scratch-75 < cd->trk[track].start)
						trackmap[i++] = track++;
					else
						trackmap[i++] = -1;
					
					if (track == cur_ntracks)
						break;
				}

				/* This only happens if track == cur_ntracks */
				while (ntracks--)
					trackmap[i++] = -1;

				while (track < cur_ntracks)
				{
					if (cd->trk[track].section < 2)
						trackmap[i++] = track;
					track++;
				}

				track = 0;
				SWALLOW_LINE(fp);
			}
			else
			{
				while (ntracks--)
				{
					fscanf(fp, "%d", &scratch);
					split_trackinfo(scratch);
				}

				for (i = 0; i < cur_ntracks; i++)
				{
					trackmap[i] = i;
					/* split_trackinfo() sets this */
					cd->trk[i].contd = 0;
				}

				SWALLOW_LINE(fp);
			}
		}

		else if (! strcmp(keyword, "track"))
		{
			char buf[502];

			getc(fp);	/* lose the space */

			/* don't overwrite existing track names. */
			/* However, overwrite them if there was a "bad" fuzzy match before */
			if ((trackmap[track] == -1 || track > (cd->ntracks + cur_nsections)) && (searching == 2))
				SWALLOW_LINE(fp)
			else if (cd->trk[trackmap[track]].songname &&
					cd->trk[trackmap[track]].songname[0])
				do
					i = getc(fp);
				while (i != '\n' && i != EOF);
			else
			{
				fgets(buf, sizeof(buf), fp);
				wm_lib_message(WM_MSG_CLASS_DB|WM_MSG_LEVEL_DEBUG, "found track %s\n", buf);
				if( (i = strlen(buf)) )
					buf[i - 1] = '\0';
				wm_strmcpy(&cd->trk[trackmap[track]].songname, buf);
			}
			track++;
		}

		else if (! strcmp(keyword, "playmode"))
			fscanf(fp, "%d", &cd->playmode);

		else if (! strcmp(keyword, "autoplay"))
			cd->autoplay = 1;

		else if (! strcmp(keyword, "cdname"))
		{
                        /* because of fuzzy matching that may change
                           the disk contents, we reset everything when
                           we find the name, in hopes that we will recover
                           most, if not all, of the information from the
                           file. */
/*
 * nasty bug was here. Was it? BUGBUGBUG
 *
 *			 wipe_cdinfo(); 
 *			 trackmap = reset_tracks();
 */

			getc(fp);	/* lose the space */
			/* don't overwrite existing cd name. */
			if (cd->cdname[0] && (searching == 2))
				do
					i = getc(fp);
				while (i != '\n' && i != EOF);
			else
			{
                                if (searching > 1)
                                {
                                    strcpy(cd->cdname, "Probably://");
				    fgets(cd->cdname + strlen(cd->cdname), sizeof(cd->cdname), fp);
                                } 
                                else 
                                {
				    fgets(cd->cdname, sizeof(cd->cdname), fp);
                                } 
				if ( (i = strlen(cd->cdname)) )
					cd->cdname[i - 1] = '\0';
			}
		}

		else if (! strcmp(keyword, "artist"))
		{
			getc(fp);	/* lose the space */
			/* don't overwrite existing artist names. */
			if (cd->artist[0])
				do
					i = getc(fp);
				while (i != '\n' && i != EOF);
			else
			{
				fgets(cd->artist, sizeof(cd->artist), fp);
				if( (i = strlen(cd->artist)) )
					cd->artist[i - 1] = '\0';
			}
		}

		else if (! strcmp(keyword, "cdvolume"))
		{
			fscanf(fp, "%d", &cd->volume);
			cd->volume = (cd->volume * 100) / 32;
		}

		else if (! strcmp(keyword, "dontplay"))
		{
			fscanf(fp, "%d", &i);
			if (trackmap[i - 1] != -1)
				cd->trk[trackmap[i - 1]].avoid = 1;
		}

		else if (! strcmp(keyword, "continue"))
		{
			if (trackmap[track - 1] != -1)
				cd->trk[trackmap[track - 1]].contd = 1;
		}

		else if (! strcmp(keyword, "volume"))
		{
			fscanf(fp, "%d", &i);
			if (trackmap[i - 1] == -1)
				SWALLOW_LINE(fp)
			else
			{
				i = trackmap[i - 1];
				fscanf(fp, "%d", &cd->trk[i].volume);
				cd->trk[i].volume = (cd->trk[i].volume*100)/32;
				if (cd->trk[i].volume > 32)
					cd->trk[i].volume = 0;
			}
		}

		else if (! strcmp(keyword, "playlist"))
		{
			getc(fp);
			fscanf(fp, "%63s", listname);

/* XXX take this out at some point */
			if (! strcmp(listname, "Default"))
				strcpy(listname, "List A");

			for (i = 0; listname[i]; i++)
				if (listname[i] == '_')
					listname[i] = ' ';

			l = new_playlist(cd, listname);
			if (l == NULL)
			{
plnomem:
				perror("playlist read");
				exit(1);
			}

			fscanf(fp, "%d", &listsize);

			l->list = malloc(sizeof(int) * (listsize + 1));
			if (l->list == NULL)
				goto plnomem;

			/* Leave out tracks that weren't in .workmandb. */
			j = 0;
			for (i = 0; i < listsize; i++)
			{
				fscanf(fp, "%d", &scratch);
				scratch = trackmap[scratch - 1];
				if (scratch != -1)
					l->list[j++] = scratch + 1;
			}

			l->list[j] = 0;
		}

		else if (! strcmp(keyword, "mark"))
		{
			int mark_val = -1, mark_namelen;
			char mark_name[32];

			fscanf(fp, "%d", &mark_val);
			if (mark_val == -1)
				goto chomp;

			if (getc(fp) != ' ')
				continue;

			fgets(mark_name, sizeof(mark_name), fp);
			if( ( mark_namelen = strlen(mark_name)) )
				mark_name[mark_namelen - 1] = '\0';

/*
			if (! strcmp(mark_name, "START"))
				set_abtimer(0, mark_val);
			else if (! strcmp(mark_name, "END"))
				set_abtimer(1, mark_val);

*/
		}

		/* Unrecognized keyword.  Put it in the right place. */
		else
		{
			char	**buf, input[BUFSIZ];

			if (track && trackmap[track - 1] == -1)
			{
				SWALLOW_LINE(fp);
				continue;
			}

			i = track ? trackmap[track - 1] : 0;
			buf = prefs ? i ? &cd->trk[i].otherrc : &cd->otherrc :
				i ? &cd->trk[i].otherdb : &cd->otherdb;
			if (firstpos == -1) {
				if (prefs) {
					buf = &otherrc;
				} else {
					goto chomp;
				} /* if() else */
                        } /* if() */
			wm_strmcat(buf, keyword);
			do {
				input[sizeof(input) - 1] = 'x';
				fgets(input, sizeof(input), fp);
				wm_strmcat(buf, input);
			} while (input[sizeof(input) - 1] != 'x');
		}
	}

	if (rclen == 0 && !searching)
		rclen = pos - rcpos;

        if (searching > 1) /* A near match has been found. Good enough. */
            searching = 0;

        cddb_struct2cur();
	return (! searching);

} /* search_db() */

/*
 * Delay some amount of time without using interval timers.
 */
void
spinwheels(int secs) {
	struct timeval	tv;

	tv.tv_usec = 0;
	tv.tv_sec = secs;
	select(0, NULL, NULL, NULL, &tv);
} /* spinwheels() */

/*
 * lockit(fd, type)
 *
 * fd    file descriptor
 * type  lock type
 *
 * Lock a file.  Time out after a little while if we can't get a lock;
 * this usually means the locking system is broken.
 *
 * Unfortunately, if there are lots of people contending for a lock,
 * this can result in the file not getting locked when it probably should.
 */
int
lockit(int fd, int type)
{
	struct flock	fl;
	int		result, timer = 0;

	if (suppress_locking)
		return (0);

	fl.l_type = type;
	fl.l_whence = 0;
	fl.l_start = 0;
	fl.l_len = 0;

	while ((result = fcntl(fd, F_SETLK, &fl)) < 0)
	{
		if (errno != EACCES || errno != EAGAIN)
			break;
		if (timer++ == 30)
		{
			errno = ETIMEDOUT;
			break;
		}

		spinwheels(1);
	}

	return (result);
} /* lockit() */

/*
 * Search all the database files and our personal preference file for
 * more information about the current CD.
 */
void
load( void )
{
	FILE		*fp;
	char		**dbfile;
	int		locked = 0;
	int		dbfound = 0, *trklist, i;
	unsigned long	dbpos;

/* This is some kind of profiling code. I don't change it
   to wm_lib_message() for now... */
#ifndef NDEBUG
	time_t		t1, t2;
	if( getenv( "WORKMAN_DEBUG" ) != NULL )
	  {
		time(&t1);
		printf("%s (%d): search start = %.0f\n", __FILE__, __LINE__, difftime(t1, (time_t)0));
        fflush(stdout);
	  }	
#endif

	dbfile = databases;

	found_in_db = 0;

	/* Turn the cd->trk array into a simple array of ints. */
	trklist = (int *)malloc(sizeof(int) * cd->ntracks);
	for (i = 0; i < cd->ntracks; i++)
		trklist[i] = cd->trk[i].start;

	do {
		if (*dbfile && idx_find_entry(*dbfile, cd->ntracks, trklist,
				cd->length * 75, 0, &dbpos) == 0)
			dbfound = 1;

		fp = *dbfile ? open_rcfile(*dbfile, "r") : NULL;
		if (fp != NULL)
		{
			if (lockit(fileno(fp), F_RDLCK))
				perror("Couldn't get read (db) lock");
			else
				locked = 1;

			if (dbfound)
				fseek(fp, dbpos, 0);

			if (search_db(fp, 0, 0, 0))
			{
				found_in_db = 1;
				cd->whichdb = *dbfile;
			}

			if (locked && lockit(fileno(fp), F_UNLCK))
				perror("Couldn't relinquish (db) lock");

			fclose(fp);
		}
	} while (*++dbfile != NULL && cd->whichdb == NULL);

#ifndef NDEBUG
	if( getenv( "WORKMAN_DEBUG" ) != NULL )
	  {
		time(&t2);
		printf("%s (%d): db search end = %.0f, elapsed = %.0f\n", __FILE__, __LINE__, difftime(t2, (time_t)0), difftime(t2, t1));
		fflush(stdout);
	  }	
#endif

	fp = rcfile ? open_rcfile(rcfile, "r") : NULL;
	if (fp != NULL)
	{
		locked = 0;
		if (lockit(fileno(fp), F_RDLCK))
			perror("Couldn't get read (rc) lock");
		else
			locked = 1;

		rcpos = 0;
		found_in_rc = search_db(fp, 1, 0, 0);
		if (! found_in_rc)
			cd->autoplay = wm_db_get_playnew();

		if (locked && lockit(fileno(fp), F_UNLCK))
			perror("Couldn't relinquish (rc) lock");

		fclose(fp);
	}

	free(trklist);

	if (cur_playnew == -1)
		cur_playnew = 0;

#ifndef NDEBUG
	if( getenv( "WORKMAN_DEBUG" ) != NULL )
	  {
		time(&t2);
		printf("%s (%d): search end = %.0f, elapsed = %.0f\n", __FILE__, __LINE__, difftime(t2, (time_t)0), difftime(t2, t1));
		fflush(stdout);
	  }	
#endif
} /* load() */

/*
 * Load program settings from the rcfile.
 */
void
load_settings( void )
{
	FILE    *fp;
	int     locked;

	fp = rcfile ? open_rcfile(rcfile, "r") : NULL;
	if (fp != NULL)
	{
		locked = 0;
		if (lockit(fileno(fp), F_RDLCK))
			perror("Couldn't get read (rc) lock");
		else
			locked = 1;

		rcpos = 0;
		found_in_rc = search_db(fp, 2, 0, 0);
		if (! found_in_rc)
			cd->autoplay = wm_db_get_playnew();

		if (locked && lockit(fileno(fp), F_UNLCK))
			perror("Couldn't relinquish (rc) lock");

		fclose(fp);
	}
} /* load_settings() */

/*
 * save_globals()
 *
 * Save the global preferences, scooting CD entries to the end if needed.
 * The assumption here is that the rcfile is locked, and that firstpos has
 * been set by a previous scan.
 */
void
save_globals(FILE *fp)
{
	char	*globes = NULL, *cdentry = NULL, temp[100];
	long	curpos;
	int	globesize, hit_cdent = 0, c = 0;

	if (otherrc)
		wm_strmcpy(&globes, otherrc);

	if (cddb.protocol)
	{
		sprintf(temp, "cddbprotocol ");
		switch(cddb.protocol)
		{
		 case 1: /* cddbp */
		    sprintf(temp + strlen(temp), "cddbp\n");
		    break;
		 case 2: /* http */
		    sprintf(temp + strlen(temp), "http\n");
		    break;
		 case 3: /* proxy */
		    sprintf(temp + strlen(temp), "proxy\n");
		    break;
		 default:
		    break;
		}
		wm_strmcat(&globes, temp);
	    
		if(cddb.mail_adress[0])
	 	{
			sprintf(temp,"cddbmailadress %s\n",
				cddb.mail_adress);
			wm_strmcat(&globes, temp);
		}

		if(cddb.cddb_server[0])
	 	{
			sprintf(temp,"cddbserver %s\n",
				cddb.cddb_server);
			wm_strmcat(&globes, temp);
		}

		if(cddb.path_to_cgi[0])
	 	{
			sprintf(temp,"cddbpathtocgi %s\n",
				cddb.mail_adress);
			wm_strmcat(&globes, temp);
		}

		if(cddb.proxy_server[0])
	 	{
			sprintf(temp,"cddbproxy %s\n",
				cddb.mail_adress);
			wm_strmcat(&globes, temp);
		}
	}

	if (cur_stopmode == 1 || cur_stopmode == 2)
	{
		sprintf(temp, "whendone %s\n", cur_stopmode == 1 ? "repeat" :
			"eject");
		wm_strmcat(&globes, temp);
	}

	if (cur_playnew == 1)
		wm_strmcat(&globes, "playnew\n");

	curpos = firstpos;
	if (curpos < 0)
		curpos = 0;

	fseek(fp, curpos, SEEK_SET);

	if (firstpos < (globesize = globes != NULL ? strlen(globes) : 0))
	{
		while (1)
		{
			temp[sizeof(temp)-1] = 'x';

			if (fgets(temp, sizeof(temp), fp) == NULL)
			{
				fseek(fp, 0, SEEK_SET);
				if (globes != NULL)
				{
					fwrite(globes, globesize, 1, fp);
					free(globes);
				}
				if (cdentry != NULL)
				{
					fwrite(cdentry, strlen(cdentry), 1, fp);
					free(cdentry);
				}
				return;
			}

			if (! strncmp(temp, "tracks ", 7))
			{
				hit_cdent = 1;
				if (curpos >= globesize)
					break;
			}

			if (! hit_cdent)
			{
				curpos += strlen(temp);
				if (temp[sizeof(temp)-1] == '\0')
					while ((c = getc(fp)) != '\n' &&
								c != EOF)
						curpos++;
				if (c == '\n')
					curpos++;

				continue;
			}

			wm_strmcat(&cdentry, temp);
			curpos += strlen(temp);
			while (temp[sizeof(temp)-1] == '\0')
			{
				temp[sizeof(temp)-1] = 'x';
				if (fgets(temp, sizeof(temp), fp) == NULL)
					break;
				wm_strmcat(&cdentry, temp);
				curpos += strlen(temp);
			}
		} 

		if (cdentry != NULL)
		{
			fseek(fp, 0, SEEK_END);
			fwrite(cdentry, strlen(cdentry), 1, fp);
			free(cdentry);
		}
	}

	if (globes != NULL)
	{
		fseek(fp, 0, SEEK_SET);
		fwrite(globes, globesize, 1, fp);
		free(globes);
	}

	while (globesize++ < curpos)
		putc('\n', fp);
} /* save_globals() */

/*
 * save_entry()
 *
 * Save the CD information to one database.
 *
 *	filename	Database to save to.
 *	pref		0 for hard data, 1 for preferences.
 *
 * If an entry for this CD exists already, overwrite it with the new entry
 * if the new entry is the same size or smaller, or with newlines if the new
 * entry is larger (in which case the new entry is appended to the file.)
 *
 * Also, if the preference information is being updated, save it to the
 * file while we've got it locked.  Scoot stuff from the beginning of
 * the file to the end as needed to facilitate this.
 *
 * XXX Preference-saving should probably be done elsewhere, like in an
 * Apply button on the Goodies popup, and in any case needs to go to a
 * different file (.Xdefaults?)
 *
 * Returns 0 on success.
 */
int
save_entry(char *filename, int pref)
{
	FILE		*fp;
	char		*buf;
	int		len, i, locked = 0;


	if( filename == NULL )
		return (-1);

	fp = open_rcfile(filename, "r+");
	if (fp == NULL)
	{
		if (errno == ENOENT)	/* doesn't exist already */
			fp = open_rcfile(filename, "w");
		if (fp == NULL)
			return (-1);
	}

	if (lockit(fileno(fp), F_WRLCK))
		perror("Warning: Couldn't get write lock");
	else
		locked = 1;

	buf = print_cdinfo(cd, pref);
	len = strlen(buf);	/* doesn't return if there's an error */

	rcpos = -1;
	search_db(fp, pref, 1, len);
	if (rcpos != -1)		/* XXX */
	{
		/*
		 * Jump to the entry's position in the database file, if
		 * it was found.
		 */
		fseek(fp, rcpos, SEEK_SET);

		if (rclen >= len && holepos == -1)
		{
			/*
			 * If the new entry will fit in the space occupied by
			 * the old one, overwrite the old one and make a hole
			 * of the appropriate size at its end.
			 *
			 * No need to update the index file in this case, as
			 * the entry's position hasn't changed.
			 */
			fputs(buf, fp);
			for (i = len; i < rclen; i++)
				fputc('\n', fp);
		}
		else
		{
			/*
			 * Overwrite the old entry with a hole and delete
			 * its pointer in the index file.
			 */
			for (i = 0; i < rclen; i++)
				fputc('\n', fp);
			idx_delete_entry(filename, cd->trk[cd->ntracks-1].start,
					0, rcpos);

			rcpos = -1;
		}
	}

	/*
	 * Old entry wasn't found, or its new version wouldn't fit where
	 * the old one was.
	 */
	if (rcpos == -1)
	{
		/*
		 * Write the new entry in a hole, if there is one,
		 * or at the end of the file.
		 */
		if (holepos >= 0)
		{
			fseek(fp, holepos, SEEK_SET);
			if (holepos < firstpos)
				firstpos = holepos;
		}
		else
		{
			fseek(fp, 0, SEEK_END);
			holepos = ftell(fp);
		}
		fputs(buf, fp);

		/*
		 * Write a new index entry for this CD.
		 */
		idx_write_entry(filename, cd->trk[cd->ntracks - 1].start,
			holepos);
	}

	if (pref)
		save_globals(fp);

	fflush(fp);

	if (locked && lockit(fileno(fp), F_UNLCK))
		perror("Warning: Couldn't relinquish write lock");

	fclose(fp);

	return (0);
} /* save_entry() */

/*
 * save()
 *
 * Save CD information to the appropriate datafile (the first file in the
 * list, unless the entry came from another database file) and to the
 * personal prefs file.
 */
int
save( void )
{

	if( wm_db_save_disabled == FALSE )
	{
		if (save_entry(rcfile, 1))
			return (0);

		if (cd->whichdb == NULL || access(cd->whichdb, W_OK))
			cd->whichdb = databases[0];

		if (save_entry(cd->whichdb, 0))
			return (0);

		return( WM_DB_SAVE_ERROR );
	} else {
		return( WM_DB_SAVE_DISABLED );
	}
} /* save() */
