/*
 *  html.c
 *  mod_musicindex
 *
 *  $Id: html.c 1005 2012-07-31 12:54:59Z varenet $
 *
 *  Created by Thibaut VARENE on Thu Mar 20 2003.
 *  Copyright (c) 2003-2006 Regis BOUDIN
 *  Copyright (c) 2003-2012 Thibaut VARENE
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License version 2.1,
 *  as published by the Free Software Foundation.
 *
 */

/**
 * @file
 * HTML (and assimilated) code generation
 *
 * @author Regis Boudin
 * @author Thibaut Varene
 * @version $Revision: 1005 $
 * @date 2003-2012
 *
 * That file groups all functions related to HTML and assimilated
 * generation, that is to say functions that are used to
 * send data to the client. It is mainly (X)HTML, but also
 * playlist (in M3U format) and RSS.
 *
 * @todo still some work on the custom table
 * @todo Replace text with eye-candy icons wherever possible
 * @todo reduce the size of this file & optimize functions
 * @todo Fix search box collide on long path names
 */

#include "html.h"
#include "playlist.h"

#include <http_core.h>

#ifdef TM_IN_SYS_TIME
#include <sys/time.h>
#else
#include <time.h> /* For podcast informations */
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif
#ifdef HAVE_DIRENT_H
#include <dirent.h>	/* opendir */
#endif
#ifdef HAVE_SETLOCALE
#include <locale.h>
#endif

static const char const Gfavicon[] = "sound.png";
static const char const Gcd_icon[] = "general.png";
static const char const Gorigcss[] = "musicindex.css";
/** Cover icon names to look for in a directory */
static const char *const Gcovericns[] = {
	"cover.jpg",
	"cover.png",
	"cover.gif",
	"folder.jpg",
	"folder.png",
	"folder.gif",
	NULL
};

/**
 * Formats the list of songs to be sent.
 *
 * This function formats a list of tracks (if any) to be sent
 * to the client, as found in the @a pack passed in argument.
 * Depending on config options, it allows streaming/shuffle etc.
 *
 * @todo Sort option for recursive search?
 * @todo Keep sort conf on userside (using cookie(s)).
 * @todo We have the basis of doing per file permissions in the Select panel stuff
 *	which also needs to be rewritten and simplified
 *
 * @param r Apache request_rec struct to handle connection details.
 * @param pack pack of songs to display.
 * @param customlist Whether the list of entries p is a custom list or not
 *
 * @return The total number of fields in a row.
 */
static unsigned short
list_songs(request_rec *r, const mu_pack *const pack, const int customlist)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	const mu_ent *q;
	register unsigned short fld = 0, j = 0;
	const char * restrict current = NULL, * restrict new = NULL;
	char * restrict codeduri = NULL;
	unsigned long tlength = 0, tsize = pack->fsize;
#ifdef HAVE_SETLOCALE
	struct lconv *clconv = localeconv();
#endif

	ap_rputs("  <tr class=\"title\">\n", r);

	if ( (customlist != 0) || (conf->options & (MI_ALLOWDWNLD | MI_ALLOWSTREAM | MI_ALLOWTARBALL)) ||
		((conf->search) && (conf->options & MI_RECURSIVE)))
		ap_rvputs(r, "   <th class=\"Select\">", _("Select"), "</th>\n", NULL);

	/* The sort option is disabled for search result, and this
	   implementation handles consecutive sort requests. TODO: keep
	   memory of sort configuration by the user (either on a per-dir basis,
	   or globally), probably using cookie(s) */

	for (fld = 0; conf->fields[fld]; fld++) {	/* Display title line */
		switch (conf->fields[fld]) {
			case SB_TITLE:
				current = "Title";	/* the style class */
				new = _("Title");	/* the field name */
				break;
			case SB_TRACK:
				current = "Track";
				new = _("Track");
				break;
			case SB_POSN:
				current = "Disc";
				new = _("Disc");
				break;
			case SB_ARTIST:
				current = "Artist";
				new = _("Artist");
				break;
			case SB_LENGTH:
				current = "Length";
				new = _("Length");
				break;
			case SB_BITRATE:
				current = "Bitrate";
				new = _("Bitrate");
				break;
			case SB_FREQ:
				current = "Freq";
				new = _("Freq");
				break;
			case SB_ALBUM:
				current = "Album";
				new = _("Album");
				break;
			case SB_DATE:
				current = "Date";
				new = _("Date");
				break;
			case SB_FILETYPE:
				current = "Filetype";
				new = _("Filetype");
				break;
			case SB_GENRE:
				current = "Genre";
				new = _("Genre");
				break;
			case SB_SIZE:
				current = "Size";
				new = _("Size");
				break;
			case SB_FILENAME:
				current = "Filename";
				new = _("Filename");
				break;
			default:
				continue;
		}

		/* This test should not be necessary. But, just to be sure... */
		if ((current == NULL) || (new == NULL))
			continue;

		/* With the quickplay option, do not show Length, freq and bitrate */
		if ((conf->options & MI_QUICKPL) && ((conf->fields[fld] == SB_LENGTH) ||
			(conf->fields[fld] == SB_BITRATE) || (conf->fields[fld] == SB_FREQ)))
			continue;

		/* XXX sorting is disabled for search results */
		ap_rvputs(r, "   <th class=\"", current, "\">", NULL);
		if ((customlist == 0) && !(conf->search))
			ap_rprintf(r, "<a href=\"?sort=%d\">%s</a>", conf->fields[fld], new);
		else
			ap_rputs(new, r);
		ap_rputs("</th>\n", r);
	}
	/* at the end of the loop, 'fld' contains the number of fields */

	ap_rputs("  </tr>\n", r);

	/* init value. No chance to see the first strcmp true with that one */
	current = "///";
	new = current;

	for (q=pack->fhead; q!= NULL; q=q->next) {
		/* If we have a recursive search request we have some work to do */
		if ((conf->search != NULL) && (customlist == 0) &&
			(conf->options & MI_RECURSIVE)) {
			new = ap_make_dirstr_parent(r->pool, q->file);

			/* We just changed directories (during search, entries
			 * are sorted on a per-dir basis) so print a nice header */
			if (strcmp(current, new)) {
				current = new;
				ap_rprintf(r, "  <tr class=\"title\">\n"
					"   <th align=\"left\" colspan=\"%d\">", fld+1);	/* "Select" => +1 */
				if ((current[0] == '\0') || (current[1] == '\0'))	/* current dir is either "" or "/" */
					ap_rputs(_("In Current Directory"), r);
				else
					ap_rvputs(r, _("In"), " <a href=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, current)), "\">", ap_escape_html(r->pool, current), "</a>", NULL);

				ap_rputs("</th>\n"
					"  </tr>\n", r);
				j = 0;
			}
		}

		if ((j++) & 1)
			ap_rputs("  <tr class=\"odd\">\n", r);
		else
			ap_rputs("  <tr class=\"even\">\n", r);

		/* prepare the "Select" panel, if any */
		if ((customlist != 0) || (conf->options & (MI_ALLOWDWNLD | MI_ALLOWSTREAM | MI_ALLOWTARBALL)) ||
			((conf->search) && (conf->options & MI_RECURSIVE))) {
			/* this should save some memory */
			codeduri = (char *)realloc(codeduri, 1 + apr_base64_encode_len(strlen(q->uri)));

			ap_rputs("   <td class=\"Select\">\n", r);

			/* Display checkbox - no matter what if we have a customlist, only if stream/tarball allowed otherwise */
			if (codeduri && (customlist || (conf->options & (MI_ALLOWSTREAM | MI_ALLOWTARBALL)))) {
				apr_base64_encode(codeduri, q->uri, strlen(q->uri));
				/* we base64 encode the uri to avoid nasty special chars problems */
				ap_rvputs(r, "    <input type=\"checkbox\" name=\"file\" value=\"",
					codeduri, "\" />\n", NULL);
			}

			if (!customlist) {
				if (q->flags & EF_ALLOWDWNLD)	/* Display [download] */
					ap_rvputs(r, "    <a class=\"dld\" href=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, q->file)), "\" title=\"", _("Download"), "\">&nbsp;</a>\n", NULL);

				if (q->flags & EF_ALLOWSTREAM)	/* Display [stream] */
					ap_rvputs(r, "    <a class=\"stream\" href=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, q->file)), "?stream\" title=\"", _("Stream"), "\">&nbsp;</a>\n", NULL);
			} else {	/* in a custom list */
				if (q->flags & EF_ALLOWTARBALL)	/* Tell that tarball download will work with this file */
					ap_rvputs(r, "    <span class=\"dldok\" title=\"", _("Download Allowed"), "\">&nbsp;</span>\n", NULL);

				if (q->flags & EF_ALLOWSTREAM)	/* Tell that streaming will work with this file */
					ap_rvputs(r, "    <span class=\"streamok\" title=\"", _("Stream Allowed"), "\">&nbsp;</span>\n", NULL);
			}

			ap_rputs("   </td>\n", r);
		}

		/* Now fill in the fields */
		for (fld = 0; conf->fields[fld]; fld++) {
			switch (conf->fields[fld]) {
				case SB_TITLE:
					ap_rvputs(r, "   <td class=\"Title\">", ap_escape_html(r->pool, q->title), "</td>\n", NULL);
					break;
				case SB_TRACK:
					if (q->track)
						ap_rprintf(r, "   <td class=\"Track\">%d</td>\n", q->track);
					else
						ap_rputs( "   <td></td>\n", r);
					break;
				case SB_POSN:
					if (q->posn)
						ap_rprintf(r, "  <td class=\"Disc\">%d</td>\n", q->posn);
					else
						ap_rputs( "   <td></td>\n", r);
					break;
				case SB_ARTIST:
					ap_rprintf(r, "   <td class=\"Artist\">%s</td>\n", q->artist ? ap_escape_html(r->pool, q->artist) : "");
					break;
				case SB_LENGTH:
					if (conf->options & MI_QUICKPL)
						break;
					if (q->length) {
						ap_rprintf(r, "   <td class=\"Length\">%d:%.2d</td>\n", q->length / 60, q->length % 60);
					}
					else
						ap_rputs( "   <td></td>\n", r);
					break;
				case SB_BITRATE:
					if (conf->options & MI_QUICKPL)
						break;
					if (q->bitrate)
						ap_rprintf(r, "   <td class=\"Bitrate\"><acronym title=\"kbps%s\">%ld</acronym></td>\n", (q->flags & EF_VBR) ? " VBR" : "", q->bitrate >> 10);
					else
						ap_rputs( "   <td></td>\n", r);
					break;
				case SB_FREQ:
					if (conf->options & MI_QUICKPL)
						break;
					if (q->freq)
#ifdef HAVE_SETLOCALE
						ap_rprintf(r, "   <td class=\"Freq\"><acronym title=\"kHz\">%d%s%d</acronym></td>\n", q->freq / 1000, clconv->decimal_point, q->freq % 1000 / 100);
#else
						ap_rprintf(r, "   <td class=\"Freq\"><acronym title=\"kHz\">%d.%d</acronym></td>\n", q->freq / 1000, q->freq % 1000 / 100);
#endif
					else
						ap_rputs( "   <td></td>\n", r);
					break;
				case SB_DATE:
					if (q->date)
						ap_rprintf(r, "   <td class=\"Date\">%d&nbsp;</td>\n", q->date);
					else
						ap_rputs( "   <td></td>\n", r);
					break;
				case SB_ALBUM:
					ap_rprintf(r, "   <td class=\"Album\">%s</td>\n", q->album ? ap_escape_html(r->pool, q->album) : "");
					break;
				case SB_FILETYPE:
					ap_rvputs(r, "   <td class=\"Filetype\">", filetype[q->filetype % FT_MAX].nametype, "</td>\n", NULL);
					break;
				case SB_GENRE:
					ap_rprintf(r, "   <td class=\"Genre\">%s</td>\n", q->genre ? ap_escape_html(r->pool, q->genre) : "");
					break;
				case SB_SIZE:
#ifdef HAVE_SETLOCALE
					ap_rprintf(r, "   <td class=\"Size\"><acronym title=\"MB\">%ld%s%ld</acronym></td>\n", q->size>>20, clconv->decimal_point, (((q->size>>10)%1024)*10)>>10);	/* get kB residue *10/1024 (instead of /102.4) */ 
#else
					ap_rprintf(r, "   <td class=\"Size\"><acronym title=\"MB\">%ld.%ld</acronym></td>\n", q->size>>20, (((q->size>>10)%1024)*10)>>10);	/* get kB residue *10/1024 (instead of /102.4) */ 
#endif
					break;
				case SB_FILENAME:
					ap_rvputs(r, "   <td class=\"Filename\">", ap_escape_html(r->pool, q->filename), "</td>\n", NULL);
					break;
				default:
					break;
			}
		}
		/* at the end of the loop, 'fld' contains the number of fields */
		ap_rputs("  </tr>\n", r);
		tlength += q->length;
	}
	
	free(codeduri);
	
	/* close the table with some useful stuff */
	ap_rprintf(r, "  <tr class=\"stfoot\">\n   <th align=\"left\" colspan=\"%d\">", fld+1);	/* Select => +1 */
	
	/* Do NOT show 'select all' if there's nothing to select... */
	if (!customlist && (conf->search) && (conf->options & MI_ALLOWSTREAM))
		ap_rvputs(r, "<input type=\"checkbox\" name=\"all\" onclick=\"selall(this)\" />",
			_("Select All"), " - ", NULL);
	
	ap_rprintf(r, "%s: %ld:%.2ld:%.2ld - %s: %ld%s%ld MB</th>\n  </tr>\n",
			_("Total Length"), tlength / 60 / 60, tlength / 60 % 60, tlength % 60,
			_("Total Size"), tsize >> 20,
#ifdef HAVE_SETLOCALE
			clconv->decimal_point,
#else
			".",
#endif
			(((tsize >> 10) % 1024) * 10) >> 10);
	
	return fld;
}

/**
 * Formats a complete url string to be sent to client.
 *
 * This function does all the URL formatting, in the form:
 * http://[user:][passwd\@]hostname[:port]/uri/
 *
 * @bug If the admin sets a conf->iceserver too big (>MAX_PREFIX) the ices url
 * will be truncated. If hostname+auth+etc is too long this function aborts... 
 * There's no user-modifiable data here, we assume admins know what they're doing. 
 * @bug It appears this function cannot deal with IPv6 numeric URLs for now.
 *	-> fixed with AP24
 *
 * @note Eventually, for the non-incecast case we should probably use
 * ap_construct_url() which is safer and exists since 1.3.
 * Its only drawback being it allocates memory (pstrcat/psprintf).
 *
 * @param r Apache request_rec struct to handle connection details.
 * @param uri A string (basically a path) to be sent to form a complete url.
 * @param command An eventual command (like ?action=playall).
 * @param html 1 ? html output : normally escaped URL output.
 */
static void send_url(request_rec *r, const char *const restrict uri, const char *const restrict command,
			const short html)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	static char prefix[MAX_PREFIX];
	static char str_port[7], strtmp[6];	/* (:)65536 + '\0' */
#ifdef BE_INSECURE
	char * restrict bufcoded, * restrict decoded;
#endif
	unsigned short l = 0;
	unsigned register i=0, j=6;

	/* If we have an icecast server, it takes precedence, in a 'staticdir' fashion.
	If we're sending an RSS feed, it's up to us to send the ?stream links. */
	if ((conf->iceserver) && !(conf->options & MI_RSS)) {
		/* icecast can only do http, so force that method */
		strcpy(prefix, "http://");	/* 7 */

		/* if we only have a port number, we assume icecast is running on the same host */
		if (conf->iceserver[0] == ':')
			strcat(prefix, ap_get_server_name(r));

		strncat(prefix, conf->iceserver, MAX_PREFIX-8);	/* 7+'\0'. if overflow -> truncated */
	}
	else {
		strcpy(prefix, ap_http_method(r));	/* 4-5 */
		strcat(prefix, "://"); /* 3 */

		if (REQUEST_USER(r)) {
#ifdef BE_INSECURE
			/* grab the auth credentials base64 encoded - apparently only for auth_basic */
			/* This is unsafe and thus disabled at compile time */
			const char *const auth = apr_table_get(r->headers_in, "Authorization");
			if (auth) {
				bufcoded = strrchr(auth, ' ');
				bufcoded++;
				decoded = (char *)malloc(1 + apr_base64_decode_len(bufcoded));
				l = apr_base64_decode(decoded, bufcoded);
				strncat(prefix, decoded, l);	/* we have "user:pass" */
				free(decoded);
			}
#else
			strncat(prefix, REQUEST_USER(r), MAX_PREFIX-10);	/* 5+3+1+'\0' */
#endif
			strcat(prefix, "@");	/* 1 */
		}

		/* safety check */
		if (unlikely((strlen(prefix) + strlen(ap_get_server_name(r))) > MAX_PREFIX-10)) {	/* 8+'\0'+safe */
			/* we're going to exceed the size of the buffer including port number */
			mi_rerror("prefix too %s", "long");
			return;
		}
		
		/* add the hostname */
		strcat(prefix, ap_get_server_name(r));

		/* add the port number no matter what. This won't hurt, and
		 * checking whether it's really needed or not is just too
		 * much overhead.
		 * ap_get_server_port() deals with UseCanonicalName
		 * for us, and thus redirected ports will be properly handled.
		 * Custom 'snprintf' implementation for speed & fun */
		l = ap_get_server_port(r);
		while (--j && l) {
			strtmp[i++] = "0123456789"[l%10];
			l /= 10;
		}
		str_port[0] = ':';
		for (j=1; i; j++)
			str_port[j] = strtmp[--i];
		str_port[j] = '\0';
		strcat(prefix, str_port); /* 2-8 */
	}

	/* add the uri and potential command */
	if (html) /* we're going to send the url in an html document */
		ap_rvputs(r, prefix, ap_escape_html(r->pool, ap_escape_uri(r->pool, uri)), NULL);
	else /* sending to a playlist... */
		ap_rvputs(r, prefix, ap_escape_uri(r->pool, uri), NULL);
	if (command)
		ap_rputs(command, r);
}

/**
 * Sends HTML page headers and top of the page.
 *
 * This function takes care of the formating of the HTML page headers.
 * It sends the data that is common to all pages generated by the handler.
 *
 * @param r Apache request_rec struct to handle connection details.
 *
 * @bug When we'll allow #mu_config->directory to be modified, have to escape html/uri.
 */
void send_head(request_rec *r)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	register short i;
	const char *dir;
	char *uri, *u;
	request_rec *subreq = NULL;
	const mu_config *localconf = NULL;

	/* this could arguably be reduced if treated directly at the setlocale()
	call in mod_musicindex.c - XXX TO TEST */
#ifdef HAVE_SETLOCALE
	if ((uri = setlocale(LC_ALL, NULL))) { /* query current locale */
		/* get the ISO 639 language code */
		if ((u = strchr(uri, '.')))
			*u = '\0';
	}
#else
	uri = "en";
#endif

	ap_rvputs(r, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
		"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" "
		"\"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n"
		"<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"", uri, "\" lang=\"", uri, "\">\n"
		"<head>\n"
		" <meta name=\"generator\" content=\"" MUSIC_HEADER_STRING "\" />\n", NULL);

	/* XXX REVISIT: restoring a feature with probably not too shabby code? */
	/* dive into user's customisations, if any */
	if ((subreq = ap_sub_req_lookup_uri(conf->directory, r, NULL)) != NULL) {
	/* XXX enough to catch forbidden access? */
		DIR		*dir;
		struct dirent	*dstruct;
		const char	*ext;

		if ((dir = opendir(subreq->filename))) {
			while ((dstruct = readdir(dir))) {
				if ((dstruct->d_name[0] != '.') &&
					(ext = strrchr(dstruct->d_name, '.')) &&
					(!strncmp(ext+1, "css", 3))) {
					/* XXX we don't make sure the files are R_OK */
					if (!strcmp(dstruct->d_name, Gorigcss))
						continue;	/* this will be dealt with later */
					if (!strcmp(dstruct->d_name, conf->css))
						ap_rputs(" <link rel=\"stylesheet\" title=\"default\"", r);
					else
						ap_rvputs(r, " <link rel=\"alternate stylesheet\" title=\"",
							ap_escape_html(r->pool, ap_escape_uri(r->pool, dstruct->d_name)),
							"\"", NULL);

					ap_rvputs(r, " type=\"text/css\" href=\"", conf->directory, "/",
						ap_escape_html(r->pool, ap_escape_uri(r->pool, dstruct->d_name)),
						"\" />\n", NULL);
				}
			}
			closedir(dir);
		}
		ap_destroy_sub_req(subreq);
	}

	/* the original CSS */
	if (!strcmp(Gorigcss, conf->css))
		ap_rputs(" <link rel=\"stylesheet\" title=\"default\"", r);
	else
		ap_rputs(" <link rel=\"alternate stylesheet\" title=\"Original\"", r);
	ap_rvputs(r, " type=\"text/css\" href=\"", conf->directory, "/", Gorigcss, "\" />\n", NULL);

	/* RSS link */
	if (conf->rss_items > 0) {
		ap_rvputs(r, " <link rel=\"alternate\" type=\"application/rss+xml\" "
			"title=\"", _("Latest titles"), "\" href=\"?action=RSS\" />\n", NULL);
		ap_rputs(" <link rel=\"alternate\" type=\"application/rss+xml\" "
			"title=\"Podcast\" href=\"?action=podcast\" />\n", r);
	}

	/* bit of javascript to handle select all */
	ap_rputs(" <script type=\"text/javascript\">\n"
		" // <![CDATA[\n"
		" function selall(mine) {\n"
		" 	for(var i=0; i<mine.form.elements.length; i++) {\n"
		"		var inpt = mine.form.elements[i];\n"
		"		var m = inpt.name.match(/-/g);\n"
		"		if ((inpt.name.substr(0,4) == 'file') && (m < 1)) {\n"
		"			inpt.checked = mine.form.all.checked;\n"
		"		}\n"
		" 	}\n"
		" }\n"
		" // ]]>\n"
		" </script>\n", r);

	/* the original favicon */
	ap_rvputs(r, " <link rel=\"shortcut icon\" href=\"", conf->directory, "/", Gfavicon, "\" />\n"
		" <link rel=\"icon\" href=\"", conf->directory, "/", Gfavicon, "\" type=\"image/ico\" />\n"
		" <title>", _("Musical index of"), " ", ap_escape_html(r->pool, r->uri), "</title>\n"
		"</head>\n\n"
		"<body>\n"
		"<!-- begin header -->\n", NULL);

	ap_rputs("<div id=\"header\">\n"
		" <div id=\"mainicon\">\n"
		"  <img alt=\"Dir\" src=\"", r);

	for (i = 0; Gcovericns[i]; i++) {
		if (access(apr_pstrcat(r->pool, r->filename, "/", Gcovericns[i], NULL), R_OK) == 0) {
			ap_rputs(Gcovericns[i], r);
			i = -1;
			break;
		}
		else if (access(apr_pstrcat(r->pool, r->filename, "/.", Gcovericns[i], NULL), R_OK) == 0) {
			ap_rvputs(r, ".", Gcovericns[i], NULL);
			i = -1;
			break;
		}
	}

	if (i > 0)
		ap_rvputs(r, conf->directory, "/", Gcd_icon, NULL);

	ap_rputs("\" />\n"
		" </div>\n", r);

	/* Display the box containing the path and the stream links */
	ap_rputs(" <div id=\"maintitle\">\n"
		"  <h1>\n", r);

	uri = apr_pstrdup(r->pool, r->uri);

	/* XXX Could use some doc... :P */
	for (u = uri; *u != '\0'; u++) {
		dir = u;
		while ((*u != '/') && (*u != '\0'))
			u++;

		if (conf->title == NULL) {
			char bk = *(++u);
			*u = '\0';
			subreq = ap_sub_req_lookup_uri(uri, r, NULL);
			*(u--) = bk;
			localconf = (mu_config*)ap_get_module_config(subreq->per_dir_config, &musicindex_module);
		}
		else {
			localconf = conf;
			subreq = NULL;
		}

		if ((localconf->options & MI_ACTIVE) || (localconf->title == NULL)) {
			if ((u == uri) && (localconf->title != NULL))
				dir = localconf->title;

			*u = '\0';
			ap_rvputs(r, "   <a href=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, uri)), "/\">", ap_escape_html(r->pool, dir), "</a>\n", NULL);
			*u = '/';

			if (*(u+1) != '\0')
				ap_rputs("   <span class=\"rarrow\">&nbsp;</span>\n", r);
		}

		if (subreq != NULL)
			ap_destroy_sub_req(subreq);
	}

	ap_rputs("  </h1>\n", r);

	if (conf->options & MI_ALLOWSTREAM) {
		ap_rvputs(r, "  <a class=\"shuffle\" "
			"href=\"?option=recursive&amp;option=shuffle&amp;action=playall\""
			" title=\"", _("Shuffle All"), "\">&nbsp;</a>\n"
			"  <a class=\"stream\" "
			"href=\"?option=recursive&amp;action=playall\""
			" title=\"", _("Stream All"), "\">&nbsp;</a>\n", NULL);
	}

	if (conf->options & MI_ALLOWTARBALL) {
		ap_rvputs(r, "  <a class=\"tarball\" "
			"href=\"?option=recursive&amp;action=tarball\""
			" title=\"", _("Download All"), "\">&nbsp;</a>\n", NULL);
	}

	if (conf->rss_items > 0) {
		ap_rvputs(r, "    <a class=\"rss\" "
			"href=\"?action=RSS\""
			" title=\"", _("RSS"), "\">&nbsp;</a>\n", NULL);
	}

	/* XXX to integrate in a better way */
	ap_rvputs(r, "    <br /><a class=\"rdir\" "
			"href=\"?action=randomdir\">[",
			_("Random subdirectory..."), "]</a>\n </div>\n", NULL);

	/* displays a search box if option is activated */
	/* XXX collision avec le header, a regler */
	if (conf->options & MI_ALLOWSEARCH) {
		ap_rvputs(r,
			" <form method=\"post\" action=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, r->uri)), "\""
				" enctype=\"application/x-www-form-urlencoded\""
				" id=\"searching\">\n"
			"  <p>\n"
			"   <input type=\"text\" name=\"search\" />\n"
			"   <br />\n"
			"   <button type=\"submit\" name=\"action\" value=\"Search\">", _("Search"), "</button>\n"
			"   <button type=\"submit\" name=\"action\" value=\"RecursiveSearch\">", _("Recursive Search"), "</button>\n"
			"   <input type=\"hidden\" name=\"action\" value=\"Search\" />\n"
			"  </p>\n"
			" </form>\n",
			NULL);
	}

	ap_rputs("</div>\n"
		"<hr />\n"
		"<!-- end header -->\n\n", r);

}

/**
 * Sends directory listing for the current folder.
 *
 * This function takes care of preparing and sending to the client the
 * list (if any) of the available directories in the current folder.
 * Depending on config options, it allows streaming/shuffle etc.
 *
 * @remark I tried moving to purely semantic coding (using either div or li,
 *	none worked. It seems CSS/browsers can't render properly a list of
 *	element of fixed width but variable height in a variable "columns"
 *	context: if an element has a greater height than the one immediately
 *	after on the same "row", then the next row will start at the right of
 *	this longer element instead of starting back at the left of the page.
 *
 * @param r Apache request_rec struct to handle connection details.
 * @param pack A pack of music entries.
 */
void send_directories(request_rec *r, const mu_pack *const pack)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	const mu_ent *q;
	unsigned short dircnt = 0, nb = pack->dirnb;

	if (nb == 0)
		return;

	ap_rputs("<!-- begin subdirs -->\n<h2>",r);
	ap_rprintf(r, _("Music Directories (%d)"), nb);
	ap_rputs("</h2>\n\n<table id=\"directories\">\n", r);

	for (q=pack->head; q && (q->filetype < 0); q = q->next) {
		/* start a new directories row */
		if (dircnt++ == 0)
			ap_rputs(" <tr>\n", r);

		ap_rvputs(r, "  <td>\n"
			"   <a href=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, q->file)),
			"\" class=\"bigdir\" title=\"", _("Open"), "\">&nbsp;</a>\n"
			"   <div>\n"
			"    <a href=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, q->file)), "\">",
			ap_escape_html(r->pool, q->title), "</a>", NULL);

		if (conf->dir_per_line > 0) {
			/* show various useful links when needed */
			ap_rputs("<br />\n", r);
			if (q->flags & EF_ALLOWSTREAM) {
				ap_rvputs(r, "    <a class=\"shuffle\" href=\"",
					ap_escape_html(r->pool, ap_escape_uri(r->pool, q->file)),
					"?option=recursive&amp;option=shuffle&amp;action=playall\""
					" title=\"", _("Shuffle"), "\">&nbsp;</a>\n"
					"    <a class=\"stream\" href=\"",
					ap_escape_html(r->pool, ap_escape_uri(r->pool, q->file)),
					"?option=recursive&amp;action=playall\""
					" title=\"", _("Stream"), "\">&nbsp;</a>\n", NULL);
			}
	
			if (q->flags & EF_ALLOWTARBALL) {
				ap_rvputs(r, "    <a class=\"tarball\" href=\"",
					ap_escape_html(r->pool, ap_escape_uri(r->pool, q->file)),
					"?option=recursive&amp;action=tarball\""
					" title=\"", _("Download"), "\">&nbsp;</a>\n", NULL);
			}
	
			if (q->flags & EF_ALLOWRSS) {
				ap_rvputs(r, "    <a class=\"rss\" href=\"",
					ap_escape_html(r->pool, ap_escape_uri(r->pool, q->file)),
					"?action=RSS\""
					" title=\"", _("RSS"), "\">&nbsp;</a>\n", NULL);
			}
		}

		ap_rputs("\n   </div>\n"
			"  </td>\n",r);

		/* end the directories row */
		if (dircnt == abs(conf->dir_per_line)) {
			dircnt = 0;
			ap_rputs(" </tr>\n", r);
		}
	}

	/* if we haven't ended on a dir_per_line row, close it here */
	if (dircnt != 0)
		ap_rputs( "</tr>\n", r);

	ap_rputs("</table>\n<hr />\n<!-- end subdirs -->\n\n", r);
}

/**
 * Sends track listing for the current folder.
 *
 * This function takes care of preparing and sending to the client the
 * list (if any) of the available songs in the current @a pack list.
 * Depending on config options, it allows streaming/shuffle etc.
 *
 * @param r Apache request_rec struct to handle connection details.
 * @param pack A pack of music entries.
 */
void send_tracks(request_rec *r, const mu_pack *const pack)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	register unsigned short nb = 0;
	unsigned long fnb = pack->filenb;
	const unsigned char *const restrict order = conf->order;

	if (fnb == 0)
		return;

	ap_rputs("<!-- begin tracks -->\n<h2>", r);

	if (conf->search)
		ap_rprintf(r, _("Result List (%ld)"), fnb);
	else
		ap_rprintf(r, _("Song List (%ld)"), fnb);

	ap_rvputs(r, "</h2>\n\n<form method=\"post\" action=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, r->uri)), "\" "
		"enctype=\"application/x-www-form-urlencoded\" id=\"tracks\">\n <table>\n", NULL);

	nb = list_songs(r, pack, 0);

	ap_rputs(" </table>\n", r);

	/* Do NOT show buttons if we do not allow anything but listing or individual downloading! */
	if ((conf->options & (MI_ALLOWSTREAM|MI_ALLOWTARBALL))) {
		ap_rputs(" <div>\n"
			"  <input type=\"hidden\" name=\"sort\" value=\"", r);
		for(nb=0; order[nb] && (nb<SB_MAX); nb++)
			ap_rprintf(r, "%c", order[nb]+'`');	/* 1+'`' = 'a'. shift everything to ascii small letters */
		ap_rvputs(r, "\" />\n"
			"  <button type=\"submit\" name=\"action\" value=\"AddToPlaylist\" class=\"playlist\">",
			_("Add To Playlist"), "</button>\n", NULL);

		if (conf->search == NULL) {
			ap_rvputs(r, "  <button type=\"submit\" name=\"action\" value=\"AddAllToPlaylist\" class=\"playlist\">",
				_("Add All To Playlist"), "</button>\n", NULL);
			if (conf->options & MI_ALLOWSTREAM)
				ap_rvputs(r, "  <button type=\"submit\" name=\"action\" value=\"ShuffleAll\">",
				_("Shuffle All"), "</button>\n"
				"  <button type=\"submit\" name=\"action\" value=\"PlayAll\">",
				_("Play All"), "</button>\n", NULL);
			if (conf->options & MI_ALLOWTARBALL)
				ap_rvputs(r, "  <button type=\"submit\" name=\"action\" value=\"DownloadAll\">",
				_("Download All"), "</button>\n", NULL);
		}

		if (conf->options & MI_ALLOWSTREAM)
			ap_rvputs(r, "  <button type=\"submit\" name=\"action\" value=\"PlaySelected\">",
				_("Play Selected"), "</button>\n", NULL);

		if (conf->options & MI_ALLOWTARBALL)
			ap_rvputs(r, "  <button type=\"submit\" name=\"action\" value=\"DownloadSelected\">",
				_("Download Selected"), "</button>\n", NULL);

		ap_rputs(" </div>\n", r);
	}

	ap_rputs("</form>\n"
		"<hr />\n"
		"<!-- end tracks -->\n\n", r);
}

/**
 * Sends track listing for the current custom playlist.
 *
 * This function takes care of preparing and sending to the client the
 * list (if any) of the available songs in the current custom @a pack list,
 * retrieved from a cookie.
 * Depending on config options, it allows streaming/shuffle etc.
 *
 * @warning Even though the buttons are here (Download All, Stream All...),
 *	custom playlists will always respect individual files permissions,
 *	meaning that one can end up with an empty playlist or tarball even
 *	though the list itself isn't empty.
 *
 * @param r Apache request_rec struct to handle connection details.
 * @param pack A pack of music entries.
 */
void send_customlist(request_rec *r, const mu_pack *const pack)
{
	register unsigned short nb = pack->filenb;

	if (!pack->fhead)
		return;

	ap_rputs("<!-- begin custom -->\n"
		"<h2>", r);
	ap_rprintf(r, _("Custom Playlist (%d)"), nb);
	ap_rputs("</h2>\n\n", r);

	ap_rvputs(r, " <form method=\"post\" action=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, r->uri)), "\" "
		"enctype=\"application/x-www-form-urlencoded\" id=\"custom\">\n  <table>\n", NULL);

	list_songs(r, pack, 1);

	ap_rvputs(r, "  </table>\n"
		"  <div>\n   <button type=\"submit\" name=\"action\" value=\"RemoveFromPlaylist\" class=\"playlist\">",
		_("Remove From Playlist"), "</button>\n"
		"   <button type=\"submit\" name=\"action\" value=\"ClearPlaylist\" class=\"playlist\">",
		_("Clear Playlist"), "</button>\n"
		"   <button type=\"submit\" name=\"action\" value=\"StreamPlaylist\" class=\"playlist\">",
		_("Stream Playlist"), "</button>\n"
#ifdef ENABLE_OUTPUT_ARCHIVE
		"   <button type=\"submit\" name=\"action\" value=\"DownloadPlaylist\" class=\"playlist\">",
		_("Download Playlist"), "</button>\n"
#endif
		, NULL);

	ap_rputs("  </div>\n"
		" </form>\n"
		"<hr />\n"
		"<!-- end custom -->\n\n", r);
}

/**
 * Sends playlist with url and EXTM3U info to the client.
 *
 * This function prepares an m3u playlist (with extra information where
 * available: length, artist, title, album) and sends it to the client.
 *
 * @param r Apache request_rec struct to handle connection details.
 * @param pack A pack of music entries.
 */
void send_playlist(request_rec *r, const mu_pack *const pack)
{
	const mu_ent *q = pack->fhead;

	if (!q)
		return;

	ap_rputs("#EXTM3U\n", r);

	for (; q; q = q->next) {
		/* do not include file in playlist if streaming is not allowed */
		if (!(q->flags & EF_ALLOWSTREAM))
			continue;

		ap_rprintf(r, "#EXTINF:%i,", q->length);
		if (q->artist)
			ap_rvputs(r, q->artist, " - ", NULL);
		ap_rvputs(r, q->title, NULL);
		if (q->album)
			ap_rvputs(r, " (", q->album, ")", NULL);
		ap_rputc('\n', r);
		send_url(r, q->uri, NULL, 0);
		ap_rputc('\n', r);
	}
}

/**
 * Sends RSS feed.
 *
 * see http://blogs.law.harvard.edu/tech/rss
 * and http://feedvalidator.org/docs/rss2.html
 *
 * @param r Apache request_rec struct to handle connection details.
 * @param pack A pack of music entries.
 */
void send_rss(request_rec *r, const mu_pack *const pack)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	const mu_ent *q = pack->fhead;
	char date_buf[32];
	struct tm time_buf;
	unsigned short limit = conf->rss_items;

	if (!q)
		return;

	ap_rputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
		"<rss ", r);

	if (conf->options & MI_PODCAST)
		ap_rputs("xmlns:itunes=\"http://www.itunes.com/dtds/podcast-1.0.dtd\" ", r);

	ap_rvputs(r, "version=\"2.0\">\n"
		" <channel>\n"
		"  <title>", _("RSS Feed for "), ap_escape_html(r->pool, r->uri), "</title>\n"
		"  <link>", NULL);
	send_url(r, r->uri, NULL, 1);

	ap_rputs("</link>\n  <description>", r);
	ap_rprintf(r, _("%d most recent songs from %s"), conf->rss_items, ap_escape_html(r->pool, r->uri));
	ap_rputs("</description>\n", r);
	
	if ((conf->options & MI_PODCAST) != 0) {
		ap_rputs("  <itunes:summary>", r);
		ap_rprintf(r, _("%d most recent songs from %s"), conf->rss_items, ap_escape_html(r->pool, r->uri));
		ap_rputs("</itunes:summary>\n", r);
	}

	ap_rputs("  <generator>" MUSIC_HEADER_STRING "</generator>\n"
		"  <docs>http://backend.userland.com/rss</docs>\n"
		"  <ttl>60</ttl>\n",
		r);

	for (; limit && q; q = q->next) {
		ap_rvputs(r, "  <item>\n"
			"   <title>", ap_escape_html(r->pool, q->title), "</title>\n", NULL);

		if (conf->options & MI_ALLOWSTREAM) {
			ap_rputs("   <link>", r);
			send_url(r, q->uri, "?stream", 1);
			ap_rputs("</link>\n", r);
		}

		if (conf->options & MI_ALLOWDWNLD) {
			ap_rputs("   <enclosure url=\"", r);
			send_url(r, q->uri, NULL, 1);
			ap_rprintf(r, "\" length=\"%lu\" type=\"%s\" />\n",
				q->size, filetype[q->filetype % FT_MAX].mimetype);

			if (conf->options & MI_PODCAST) {
				ap_rputs("   <guild>", r);
				send_url(r, q->uri, NULL, 1);
				ap_rputs("</guild>\n", r);
				localtime_r(&q->mtime, &time_buf);
				strftime(date_buf, 32, "%a, %e %b %Y %H:%M:%S %z", &time_buf);
				ap_rprintf(r, "   <pubDate>%s</pubDate>\n", date_buf);
				if (q->length)
					ap_rprintf(r, "<itunes:duration>%u:%.2u</itunes:duration>",
							q->length / 60, q->length % 60);
			}
		}

		ap_rvputs(r, "   <description>\n    ", _("Artist"), " | ", _("Album"), " | ", _("Track"), " | ", _("Disc"),
					" | ", _("Length"), " | ", _("Genre"), " | ", _("Bitrate"), " | ", _("Freq"), " | ", _("Filetype"),
					" | ", _("Size"), "<br />\n", NULL);
		ap_rprintf(r, "    %s | %s | ", q->artist ? ap_escape_html(r->pool, q->artist) : "",
				q->album ? ap_escape_html(r->pool, q->album) : "");
		if (q->track)
			ap_rprintf(r, "%u", q->track);
		ap_rputs(" | ", r);
		if (q->posn)
			ap_rprintf(r, "%u", q->posn);
		ap_rputs(" | ", r);
		if (q->length)
			ap_rprintf(r, "%u:%.2u", q->length / 60, q->length % 60);
		ap_rputs(" | ", r);
		ap_rprintf(r, "%s | ", q->genre ? ap_escape_html(r->pool, q->genre) : "");
		if (q->bitrate)
			ap_rprintf(r, "%lu %s", q->bitrate >> 10, (q->flags & EF_VBR) ? "VBR" : "");
		ap_rputs(" | ", r);
		if (q->freq)
			ap_rprintf(r, "%u", q->freq);
		ap_rputs(" | ", r);
		ap_rprintf(r, "%s | %lu\n", filetype[q->filetype % FT_MAX].nametype, q->size);
		ap_rputs("   </description>\n"
			"  </item>\n", r);

		limit--;
	}
	ap_rputs(" </channel>\n"
		"</rss>\n", r);
}

/**
 * Sends a random directory location.
 *
 * This function constructs a new URL relative to the current directory,
 * corresponding to a random subdirectory. It might stop at an arbitrary
 * sublevel, even at the current dir.
 *
 * XXX A better (or at least different) implementation would only accounts for
 * actual folders, and setup a random threshold level at which the function stops
 * recursing.
 *
 * @bug A forseable bug is that this function can send you to a directory which
 * access may be denied by .htaccess
 *
 * @param r Apache request_rec struct to handle connection details.
 */
void send_randomdir(request_rec *r)
{
	const char *filename, *uri, *nextfile, *nexturi;
	unsigned short nb, test, i;
	unsigned int seed = time(NULL);
	DIR *dir;
	struct dirent *dirent;

	filename = apr_pstrdup(r->pool, r->filename);
	uri = apr_pstrdup(r->pool, r->uri);
	
	while (1) {
		/* XXX this is ugly */
		filename = apr_pstrcat(r->pool, filename, "/", NULL);
	
		/* open the current directory for scanning */
		dir = opendir(filename);
		
		/* XXX TODO optimize the loops */
		/* count the number of *valid* dir entries */
		nb = 0;
		while ((dirent = readdir(dir)))
			if ((dirent->d_name[0] != '.') && (access(r->filename, R_OK|X_OK) == 0))
				nb++;

		/* if nb == 0, we've got an empty directory. Don't go any further */
		if (!nb) {
			closedir(dir);
			apr_table_setn(r->headers_out, "Location", ap_construct_url(r->pool, ap_escape_uri(r->pool, uri), r));
			return;
		}
		
		/* once it's done, go back to the begining of the dir stream */
		rewinddir(dir);
		
		/* generate a random position, using high-order bits. */
		test = (int)(1.0*nb*rand_r(&seed)/(RAND_MAX+1.0));

		/* cycle through the current dir until we've reached EOF or random position */
		i = 0;
		while((i <= test) && (dirent = readdir(dir)))
			if ((dirent->d_name[0] != '.') && (access(r->filename, R_OK|X_OK) == 0))
				i++;
		
		/* construct a new filename with the current dirent */
		nextfile = apr_pstrcat(r->pool, filename, dirent->d_name, NULL);
		nexturi = apr_pstrcat(r->pool, uri, dirent->d_name, NULL);

		/* done with the dir listing, close it */
		closedir(dir);

		/* if the new filename is a directory, start the loop again, else, break */
		if (!ap_is_directory(r->pool, nextfile)) {
			/* XXX this is probably not good either */
			apr_table_setn(r->headers_out, "Location", ap_construct_url(r->pool, ap_escape_uri(r->pool, uri), r));
			return;
		}
		else {
			/* XXX gack! */
			filename = nextfile;
			uri = apr_pstrcat(r->pool, nexturi, "/", NULL);
		}
	}
}

/**
 * Sends HTML page footers.
 *
 * This function takes care of the formating of the HTML page footers.
 * It sends the data that is common to all pages generated by the handler.
 *
 * @param r Apache request_rec struct to handle connection details.
 * @param tvbegin when available, a timeval struct containing timing info for
 *	the beginning of the page generation.
 * @param tvprocess when available a timeval struct containing end of processing time
 */
void send_foot(request_rec *r, const STRUCTTV *tvbegin, const STRUCTTV *tvprocess)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	char *icn = NULL;
	request_rec *subreq = ap_sub_req_lookup_uri(conf->directory, r, NULL);
#ifdef HAVE_GETTIMEOFDAY
	struct timeval tvend, tvres;
#ifdef HAVE_SETLOCALE
	struct lconv *tlconv = NULL;
#endif
#endif
	
	ap_rputs("<!-- begin footer -->\n"
		"<!-- mod_musicindex v." MUSIC_VERSION_STRING " -->\n"
		"<!-- Authors: " MUSIC_AUTHORS_STRING " -->\n"
		"<div id=\"footer\">\n"
		" <div id=\"valid\">\n", r);
	/* if we have local icons, use them */
	/* valid xhtml 1.1 */
	if (subreq && !access(apr_pstrcat(r->pool, subreq->filename, "/valid-xhtml11", NULL), R_OK)) {
		icn = apr_pstrcat(r->pool, conf->directory, "/valid-xhtml11", NULL);
		ap_rvputs(r, "  <img src=\"", icn, "\" alt=\"Valid XHTML 1.1!\" />\n", NULL);
	}
	else
		ap_rputs("  <a href=\"http://validator.w3.org/check?uri=referer\">\n"
			 "   <img src=\"http://www.w3.org/Icons/valid-xhtml11\"\n"
			 "    alt=\"Valid XHTML 1.1!\" height=\"31\" width=\"88\" />\n"
			 "  </a>\n", r);
	/* valid CSS */
	if (subreq && !access(apr_pstrcat(r->pool, subreq->filename, "/vcss", NULL), R_OK)) {
		icn = apr_pstrcat(r->pool, conf->directory, "/vcss", NULL);
		ap_rvputs(r, "  <img src=\"", icn, "\" alt=\"Valid CSS!\" />\n", NULL);
	}
	else
		ap_rputs("  <a href=\"http://jigsaw.w3.org/css-validator/check/referer\">\n"
			 "   <img src=\"http://jigsaw.w3.org/css-validator/images/vcss\"\n"
			 "    alt=\"Valid CSS!\" height=\"31\" width=\"88\" />\n"
			 "  </a>\n", r);
	/* Valid RSS */
	if (subreq && !access(apr_pstrcat(r->pool, subreq->filename, "/valid-rss.png", NULL), R_OK)) {
		icn = apr_pstrcat(r->pool, conf->directory, "/valid-rss.png", NULL);
		ap_rvputs(r, "  <img src=\"", icn, "\" alt=\"[Valid RSS]\" title=\"Validate my RSS feed\" />\n", NULL);
	}
	else
		ap_rputs("  <a href=\"http://validator.w3.org/feed/check.cgi?uri=referer\">\n"
			 "   <img src=\"http://validator.w3.org/feed/images/valid-rss.png\"\n"
			 "    alt=\"[Valid RSS]\" title=\"Validate my RSS feed\" />\n"
			 "  </a>\n", r);
	ap_rputs(" </div>\n", r);

#ifdef HAVE_GETTIMEOFDAY
	/* timing */
	gettimeofday(&tvend, NULL);
	timersub(&tvend, tvbegin, &tvres);
	ap_rputs(" <div id=\"timing\">", r);
	ap_rprintf(r, "<!-- processing completed in %u.%.6u s -->", tvprocess->tv_sec, tvprocess->tv_usec);
#ifdef HAVE_SETLOCALE
	tlconv = localeconv();
	ap_rprintf(r, _("in %u%s%.6u s"), tvres.tv_sec, tlconv->decimal_point, tvres.tv_usec);
#else
	ap_rprintf(r, _("in %u.%.6u s"), tvres.tv_sec, tvres.tv_usec);
#endif
	ap_rputs("</div>\n", r);
#endif

	/* Signature */
	ap_rputs(" <div id=\"name\">"
		"<a href=\"http://freecode.com/projects/musicindex/\">"
		"MusicIndex v." MUSIC_VERSION_STRING "</a></div>\n"
		"</div>\n"
		"<!-- end footer -->\n\n"
		"</body>\n"
		"</html>", r);
}
