diff --git a/Doc/cmus.txt b/Doc/cmus.txt
index 0492551..35b12af 100644
--- a/Doc/cmus.txt
+++ b/Doc/cmus.txt
@@ -527,7 +527,7 @@ win-add-q (*e*)
 
 	Analogous to *:add -q*
 
-win-bottom (*g*, *end*)
+win-bottom (*G*, *end*)
 	Goto bottom of the current window.
 
 win-down (*j*, *down*)
diff --git a/Makefile b/Makefile
index c0abfdb..37e41c6 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION = 2.1.0
+VERSION = 2.1.0-ft
 
 all: main plugins man
 
@@ -7,8 +7,10 @@ include scripts/lib.mk
 
 CFLAGS += -D_FILE_OFFSET_BITS=64
 
-input.o main.o ui_curses.o: .version
-input.o main.o ui_curses.o: CFLAGS += -DVERSION=\"$(VERSION)\"
+CMUS_LIBS = $(PTHREAD_LIBS) $(NCURSES_LIBS) $(ICONV_LIBS) $(DL_LIBS) -lm $(COMPAT_LIBS)
+
+as.o input.o main.o ui_curses.o: .version
+as.o input.o main.o ui_curses.o: CFLAGS += -DVERSION=\"$(VERSION)\"
 main.o server.o: CFLAGS += -DDEFAULT_PORT=3000
 
 .version: Makefile
@@ -17,23 +19,36 @@ main.o server.o: CFLAGS += -DDEFAULT_PORT=3000
 
 # programs {{{
 cmus-y := \
-	browser.o buffer.o cmdline.o cmus.o command_mode.o comment.o \
+	as.o browser.o buffer.o cmdline.o cmus.o command_mode.o comment.o \
 	db.o debug.o editable.o expr.o filters.o \
 	format_print.o glob.o help.o history.o http.o input.o \
-	keys.o lib.o load_dir.o locking.o mergesort.o misc.o options.o \
+	keys.o lib.o load_dir.o locking.o network.o md5.o mergesort.o misc.o options.o \
 	output.o pcm.o pl.o play_queue.o player.o \
 	read_wrapper.o server.o search.o \
-	search_mode.o spawn.o tabexp.o tabexp_file.o \
+	search_mode.o spawn.o strlib.o tabexp.o tabexp_file.o \
 	track.o track_db.o track_info.o tree.o uchar.o ui_curses.o window.o \
 	worker.o xstrjoin.o
 
 $(cmus-y): CFLAGS += $(PTHREAD_CFLAGS) $(NCURSES_CFLAGS) $(ICONV_CFLAGS) $(DL_CFLAGS)
 
 cmus: $(cmus-y) file.o path.o prog.o xmalloc.o
-	$(call cmd,ld,$(PTHREAD_LIBS) $(NCURSES_LIBS) $(ICONV_LIBS) $(DL_LIBS) -lm $(COMPAT_LIBS))
+	$(call cmd,ld,$(CMUS_LIBS))
 
 cmus-remote: main.o file.o path.o prog.o xmalloc.o
 	$(call cmd,ld,$(COMPAT_LIBS))
+
+# cygwin compat
+DLLTOOL=dlltool
+
+libcmus-$(CONFIG_CYGWIN) := libcmus.a
+
+libcmus.a: $(cmus-y) file.o path.o prog.o xmalloc.o
+	$(LD) -shared -o cmus.exe -Wl,--out-implib=libcmus.a -Wl,--base-file,cmus.base \
+		-Wl,--export-all-symbols -Wl,--no-whole-archive $^ $(CMUS_LIBS)
+	$(DLLTOOL) --output-def cmus.def --dllname cmus.exe --export-all-symbols $^
+	$(DLLTOOL) --base-file cmus.base --dllname cmus.exe --input-def cmus.def --output-exp cmus.exp
+	$(LD) -o cmus.exe -Wl,cmus.exp $^ $(CMUS_LIBS)
+
 # }}}
 
 # input plugins {{{
@@ -66,31 +81,31 @@ $(vorbis-objs):		CFLAGS += $(VORBIS_CFLAGS)
 $(mp4-objs):		CFLAGS += $(MP4_CFLAGS)
 $(aac-objs):		CFLAGS += $(AAC_CFLAGS)
 
-flac.so: $(flac-objs)
+flac.so: $(flac-objs) $(libcmus-y)
 	$(call cmd,ld_dl,$(FLAC_LIBS))
 
-mad.so: $(mad-objs)
-	$(call cmd,ld_dl,$(MAD_LIBS))
+mad.so: $(mad-objs) $(libcmus-y)
+	$(call cmd,ld_dl,$(MAD_LIBS) $(ICONV_LIBS))
 
-mikmod.so: $(mikmod-objs)
+mikmod.so: $(mikmod-objs) $(libcmus-y)
 	$(call cmd,ld_dl,$(MIKMOD_LIBS))
 
-modplug.so: $(modplug-objs)
+modplug.so: $(modplug-objs) $(libcmus-y)
 	$(call cmd,ld_dl,$(MODPLUG_LIBS))
 
-mpc.so: $(mpc-objs)
+mpc.so: $(mpc-objs) $(libcmus-y)
 	$(call cmd,ld_dl,$(MPC_LIBS))
 
-vorbis.so: $(vorbis-objs)
+vorbis.so: $(vorbis-objs) $(libcmus-y)
 	$(call cmd,ld_dl,$(VORBIS_LIBS))
 
-wav.so: $(wav-objs)
+wav.so: $(wav-objs) $(libcmus-y)
 	$(call cmd,ld_dl,)
 
-mp4.so: $(mp4-objs)
+mp4.so: $(mp4-objs) $(libcmus-y)
 	$(call cmd,ld_dl,$(MP4_LIBS))
 
-aac.so: $(aac-objs)
+aac.so: $(aac-objs) $(libcmus-y)
 	$(call cmd,ld_dl,$(AAC_LIBS))
 
 # }}}
@@ -101,33 +116,39 @@ arts-objs		:= arts.lo
 oss-objs		:= oss.lo mixer_oss.lo
 sun-objs		:= sun.lo mixer_sun.lo
 ao-objs			:= ao.lo
+waveout-objs		:= waveout.lo
 
 op-$(CONFIG_ALSA)	+= alsa.so
 op-$(CONFIG_ARTS)	+= arts.so
 op-$(CONFIG_OSS)	+= oss.so
 op-$(CONFIG_SUN)	+= sun.so
 op-$(CONFIG_AO)		+= ao.so
+op-$(CONFIG_WAVEOUT)	+= waveout.so
 
 $(alsa-objs): CFLAGS	+= $(ALSA_CFLAGS)
 $(arts-objs): CFLAGS	+= $(ARTS_CFLAGS)
 $(oss-objs):  CFLAGS	+= $(OSS_CFLAGS)
 $(sun-objs):  CFLAGS	+= $(SUN_CFLAGS)
 $(ao-objs):   CFLAGS	+= $(AO_CFLAGS)
+$(waveout-objs): CFLAGS += $(WAVEOUT_CFLAGS)
 
-alsa.so: $(alsa-objs)
+alsa.so: $(alsa-objs) $(libcmus-y)
 	$(call cmd,ld_dl,$(ALSA_LIBS))
 
-arts.so: $(arts-objs)
+arts.so: $(arts-objs) $(libcmus-y)
 	$(call cmd,ld_dl,$(ARTS_LIBS))
 
-oss.so: $(oss-objs)
+oss.so: $(oss-objs) $(libcmus-y)
 	$(call cmd,ld_dl,$(OSS_LIBS))
 
-sun.so: $(sun-objs)
+sun.so: $(sun-objs) $(libcmus-y)
 	$(call cmd,ld_dl,$(SUN_LIBS))
 
-ao.so: $(ao-objs)
+ao.so: $(ao-objs) $(libcmus-y)
 	$(call cmd,ld_dl,$(AO_LIBS))
+
+waveout.so: $(waveout-objs) $(libcmus-y)
+	$(call cmd,ld_dl,$(WAVEOUT_LIBS))
 # }}}
 
 # man {{{
@@ -147,7 +168,7 @@ quiet_cmd_ttman = MAN    $@
 
 data		= $(wildcard data/*)
 
-clean		+= *.o *.lo *.so cmus cmus-remote Doc/*.o Doc/ttman Doc/*.1
+clean		+= *.o *.lo *.so cmus libcmus.a cmus.def cmus.base cmus.exp cmus-remote Doc/*.o Doc/ttman Doc/*.1
 distclean	+= .version config.mk config/*.h tags
 
 main: cmus cmus-remote
diff --git a/as.c b/as.c
new file mode 100644
index 0000000..d737b96
--- /dev/null
+++ b/as.c
@@ -0,0 +1,931 @@
+/*
+ * Copyright 2004-2006 Timo Hirvonen
+ *
+ * as.[ch] by Frank Terbeck <ft@bewatermyfriend.de>
+ *
+ * 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., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ */
+
+/*
+ * see as.h and http://www.audioscrobbler.net
+ */
+
+#include <pthread.h>
+#include <time.h>
+
+#include "as.h"
+#include "cmus.h"
+#include "debug.h"
+#include "list.h"
+#include "locking.h"
+#include "md5.h"
+#include "network.h"
+#include "player.h"
+#include "strlib.h"
+#include "utils.h"
+
+struct as_authinfo as_authinfo;
+struct as_hsinfo as_hsinfo;
+const char *as_numbers[] = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" };
+time_t as_lasttimestamp = 0;
+int as_enable = 0;
+int running = 0;
+
+static pthread_mutex_t as_mutex = CMUS_MUTEX_INITIALIZER;
+static pthread_t as_thread;
+static LIST_HEAD(as_queue_head);
+
+static unsigned int as_count_entries(void)
+{ /*{{{*/
+	/* make sure as_lock() was called. */
+	struct as_queue *entry;
+	int count=0;
+	list_for_each_entry(entry, &as_queue_head, node) {
+		++count;
+	}
+	return(count);
+} /*}}}*/
+
+static struct as_queue *as_get_last_entry(void)
+{ /*{{{*/
+	/* make sure as_lock() was called. */
+	struct as_queue *entry;
+	list_for_each_entry(entry, &as_queue_head, node) { }
+	return(entry);
+} /*}}}*/
+
+static void as_urlencode(string_t *str)
+{ /*{{{*/
+	const char *transtab[] =
+	{ /*{{{*/
+		" ",	"%20", /* 00 01 */
+		"!",    "%21", /* 02 03 */
+		"\"",	"%22", /* 04 05 */
+		"#",	"%23", /* 06 07 */
+		"%",	"%25", /* 08 09 */
+		"&",	"%26", /* 10 11 */
+		"'",	"%27", /* 12 13 */
+		"(",	"%28", /* 14 15 */
+		")",	"%29", /* 16 17 */
+		"*",	"%2A", /* 18 19 */
+		"-",    "%2D", /* 20 21 */
+		"/",	"%2F", /* 22 23 */
+		":",	"%3A", /* 24 25 */
+		";",	"%3B", /* 26 27 */
+		"<",	"%3C", /* 28 29 */
+		"=",	"%3D", /* 30 31 */
+		">",	"%3E", /* 32 33 */
+		"?",	"%3F", /* 34 35 */
+		"@",	"%40", /* 36 37 */
+		"[",	"%5B", /* 38 39 */
+		"\\",	"%5C", /* 40 41 */
+		"]",	"%5D", /* 42 43 */
+		"^",	"%5E", /* 44 45 */
+		"`",	"%60", /* 46 47 */
+		"{",	"%7B", /* 48 49 */
+		"|",	"%7C", /* 50 51 */
+		"}",	"%7D", /* 52 53 */
+		"~",	"%7E", /* 54 55 */
+	}; /*}}}*/
+	int i, e, idx=-1;
+	for (i=0; i<=str->len-1; ++i) {
+		if (mustquote(str->s[i])) {
+			for (e=0; e<=55; e+=2) {
+				if (transtab[e][0] == str->s[i]) {
+					idx=e+1;
+					break;
+				}
+			}
+			if (idx >= 0)
+				str_nreplace(str, i, 1, transtab[idx], 3);
+		}
+	}
+} /*}}}*/
+
+static void as_update_timestamp(void)
+{ /*{{{*/
+	as_lasttimestamp=time(NULL);
+} /*}}}*/
+
+static void as_handshake(void)
+{ /*{{{*/
+	int sockfd;
+	unsigned long int idx, odx;
+	char buf[AS_BUFSIZE+1];
+	size_t n;
+	string_t msg, tmp;
+
+	/* no authentication values set, don't do anything */
+	if (as_authinfo.user[0] == '\0' || as_authinfo.pass[0] == '\0')
+		return;
+
+	/* prepare handshake request */
+	str_init(&msg);
+	str_init(&tmp);
+	str_ncat(&msg, "GET ", 4); str_scat(&msg, AS_HS_URI);
+	str_ncat(&msg, "&p=", 3);  str_scat(&msg, AS_PROTO);
+	str_ncat(&msg, "&c=", 3);  str_ncat(&msg, AS_CLIENT_ID, 3);
+	str_ncat(&msg, "&v=", 3);  str_scat(&msg, VERSION);         /* cmus' Version, that is */
+	as_lock();
+	str_scat(&tmp, as_authinfo.user);
+	as_unlock();
+	as_urlencode(&tmp);
+	str_ncat(&msg, "&u=", 3);  str_cat(&msg, &tmp);
+	str_ncat(&msg, " HTTP/1.1\r\nHost: ", 17); str_scat(&msg, AS_HOSTNAME);
+	str_ncat(&msg, "\r\nConnection: CLOSE\r\n\r\n", 23);
+
+	/* connect() */
+	if ( (sockfd = tcp_connect(AS_HOSTNAME, AS_HOSTPORT)) < 0 ) {
+		d_print("tcp_connect() failed!\n");
+		as_hsinfo.failed++;
+		goto cleanup;
+	}
+
+	/* submit our handshake request */
+	xwriten(sockfd, msg.s, msg.len);
+
+	msg.s[0] = '\0';
+	msg.len  = 0;
+
+	/* read server's response */
+	for (;;) {
+		if ( (n = xreadn(sockfd, buf, AS_BUFSIZE) ) <= 0)
+			break;
+		buf[n] = '\0';
+		str_ncat(&msg, buf, n);
+	}
+	xclose(sockfd);
+
+#ifdef AS_HEAVY_DEBUG
+	d_print("Serverreply:\n\"%s\"\n", msg.s);
+#endif
+
+	/* parse the response we read */
+	as_lock();
+	if (str_ngetidx(&msg, 0, "\r\n\r\n", 4, &idx)) {
+		str_nreplace(&msg, 0, idx+4, "", 0);
+	}
+	else {
+		d_print("broken reply, couldn't find end of headers (\\r\\n\\r\\n)\n");
+		as_hsinfo.failed++;
+		goto cleanup;
+	}
+
+	/*
+	 * UPTODATE/UPDATE/FAILED/BADUSER
+	 */
+	idx=odx=0;
+	if (!str_ngetidx(&msg, odx, "\n", 1, &idx)) {
+		d_print("Broken input: \"%s\"\n", msg.s);
+		as_hsinfo.failed++;
+		goto cleanup;
+	}
+	str_ncpy(&tmp, msg.s+odx, idx-odx);
+	if      (str_ngetidx(&tmp, 0, "UPTODATE", 8, NULL)) {
+		d_print("received UPTODATE, good.\n");
+	}
+	else if (str_ngetidx(&tmp, 0, "UPDATE", 6, NULL)) {
+		d_print("received UPDATE, get a new client, baby.\n");
+	}
+	else if (str_ngetidx(&tmp, 0, "FAILED", 6, NULL)) {
+		d_print("received FAILED, crap :-/\n");
+		as_hsinfo.failed++;
+		goto cleanup;
+	}
+	else if (str_ngetidx(&tmp, 0, "BADUSER", 7, NULL)) {
+		d_print("received BADUSER, err, check as_user setting.\n");
+		as_hsinfo.failed++;
+		goto cleanup;
+	}
+	else {
+		d_print("Unknown response: \"%s\"\n", tmp.s);
+		as_hsinfo.failed++;
+		goto cleanup;
+	}
+
+	/*
+	 * -challenge-
+	 */
+	odx=idx+1;
+	if (!str_ngetidx(&msg, odx, "\n", 1, &idx)) {
+		d_print("Broken input: \"%s\"\n", msg.s);
+		as_hsinfo.failed++;
+		goto cleanup;
+	}
+	str_ncpy(&tmp, msg.s+odx, idx-odx);
+	str_cpy(&(as_hsinfo.challenge), &tmp);
+
+	/*
+	 * -url-to-submit-script
+	 */
+	odx=idx+1;
+	if (!str_ngetidx(&msg, odx, "\n", 1, &idx)) {
+		d_print("Broken input: \"%s\"\n", msg.s);
+		as_hsinfo.failed++;
+		goto cleanup;
+	}
+	str_ncpy(&tmp, msg.s+odx, idx-odx);
+	odx=idx+1;
+	if (str_ngetidx(&tmp, 0, "://", 3, &idx)) {
+		if (idx+3 <= tmp.len)
+			str_nreplace(&tmp, 0, idx+3, "", 0);
+	}
+	else
+		d_print("hrm, no service mnemonic? (%s)\n)", tmp.s);
+
+	if (str_ngetidx(&tmp, 0, "/", 1, &idx) && (idx <= tmp.len)) {
+		str_ncpy(&as_hsinfo.path, tmp.s+idx, tmp.len-idx);
+		str_nreplace(&tmp, idx, tmp.len-idx, "", 0);
+	}
+	else {
+		d_print("um, no path? (%s)\n", tmp.s);
+		as_hsinfo.failed++;
+	}
+
+	if (str_ngetidx(&tmp, 0, ":", 1, &idx)) {
+		strncpy(as_hsinfo.port, tmp.s+idx+1, (tmp.len-idx < AS_MAXTMPLEN ? tmp.len-idx : AS_MAXTMPLEN));
+		as_hsinfo.port[(tmp.len-idx < AS_MAXTMPLEN ? tmp.len-idx : AS_MAXTMPLEN)] = '\0';
+	}
+	else {
+		d_print("well, no port definition? That, would be okay, default: 80. (%s)", tmp.s);
+		as_hsinfo.port[0] = '8'; as_hsinfo.port[1] = '0'; as_hsinfo.port[2] = '\0';
+	}
+	str_ncpy(&(as_hsinfo.host), tmp.s, idx);
+
+#ifdef AS_HEAVY_DEBUG
+	d_print("debug as_hsinfo:\n\thost: \"%s\"\n\tpath: \"%s\"\n\tport: \"%s\"\n",
+			as_hsinfo.host.s, as_hsinfo.path.s, as_hsinfo.port);
+#endif
+
+	/*
+	 * -INTERVAL-
+	 */
+	odx=idx+1;
+	if (!str_ngetidx(&msg, odx, "\n", 1, &idx)) {
+		d_print("Broken input: \"%s\"\n", msg.s);
+		goto cleanup;
+	}
+	str_ncpy(&tmp, msg.s+odx, idx-odx);
+	if (str_ngetidx(&tmp, 0, "INTERVAL ", 9, NULL)) {
+		as_hsinfo.interval=atoi(tmp.s+9);
+		d_print("Our Interval is: %d\n", (int)as_hsinfo.interval);
+	}
+
+	/* handshake suceeded, all values set. */
+	as_hsinfo.need_handshake=0;
+
+cleanup:
+	as_unlock();
+	str_free(&tmp);
+	str_free(&msg);
+} /*}}}*/
+
+static int as_submit_allowed(void)
+{ /*{{{*/
+	/*
+	 * honor INTERVAL response from the server
+	 */
+	const time_t now = time(NULL);
+	time_t at;
+	as_lock();
+	at = as_hsinfo.interval + as_lasttimestamp;
+	as_unlock();
+	if (now >= at)
+		return(1);
+	return(0);
+} /*}}}*/
+
+static int as_submit_forced(void)
+{ /*{{{*/
+	/*
+	 * queue maximum is 10, so we force submission at 8 entries
+	 */
+	unsigned int n;
+	as_lock();
+	n = as_count_entries();
+	as_unlock();
+	if (n >= AS_FORCE_SUBMIT)
+		return(1);
+	return(0);
+} /*}}}*/
+
+static void as_del_queue(struct list_head *item)
+{ /*{{{*/
+	/* make sure as_lock() was called. */
+	struct as_queue *tmp;
+
+	tmp=list_entry(item, struct as_queue, node);
+	d_print("     string: %s\n",  tmp->s.s);
+	d_print("     NEEDRN: %d\n",  tmp->needrenumber);
+	d_print("     SUBMIT: %d\n",  tmp->sc);
+	d_print("     num   : %d\n",  tmp->num);
+	str_free(&(tmp->s));
+	list_del(item);
+	free(tmp);
+} /*}}}*/
+
+static void as_destroy_queue(int scmask)
+{ /*{{{*/
+	/* make sure as_lock() was called. */
+	struct as_queue *pos, *tmp;
+
+	list_for_each_entry_safe(pos, tmp, &as_queue_head, node) {
+		if (pos->sc & scmask)
+			as_del_queue(&(pos->node));
+	}
+} /*}}}*/
+
+static void as_renumberentry(string_t *entry, unsigned short int num)
+{ /*{{{*/
+	unsigned long idx, odx = 0;
+	while (str_ngetidx(entry, odx, "]=", 2, &idx)) {
+		if (idx > 0) {
+			d_print("replacing %c with %c\n", entry->s[idx-1], as_numbers[num][0]);
+			entry->s[idx-1] = as_numbers[num][0];
+		}
+		odx=idx+2;
+	}
+} /*}}}*/
+
+static unsigned short int as_uint2string(unsigned int integer, char *str)
+{ /*{{{*/
+	unsigned short int dn, i, digit;
+	unsigned int tmp;
+
+	if (integer >= 0 && integer <= 9) {
+		dn=1;
+		str[0] = as_numbers[integer][0];
+		str[1] = '\0';
+	}
+	else {
+		dn=0;
+		tmp=integer;
+		while (tmp) {
+			++dn;
+			tmp/=10;
+		}
+	}
+	for (i=dn; i > 0 ; --i) {
+		digit=integer%10;
+		integer/=10;
+		str[i-1] = as_numbers[digit][0];
+	}
+	str[dn] = '\0';
+	return(dn);
+} /*}}}*/
+
+static void as_digest2hex(unsigned char *hex, const unsigned char *digest)
+{ /*{{{*/
+	char hc[] = "0123456789abcdef";
+	unsigned int i, e=0;
+
+	memset(hex, 0, 33*sizeof(unsigned char));
+
+	for(i = 0; i < 16; i++) {
+		hex[e++] = hc[(digest[i] >> 4) & 0x0f];
+		hex[e++] = hc[ digest[i]       & 0x0f];
+	}
+	hex[32] = '\0';
+} /*}}}*/
+
+static void as_submit(void)
+{ /*{{{*/
+	string_t msg_header, msg_content, stmp, msg;
+	struct as_queue *entry;
+	unsigned char digest[16], tmp[65], md5response[33];
+	char i2s[AS_MAXTMPLEN];
+	char buf[AS_BUFSIZE];
+	unsigned long int idx, odx, submissions=0;
+	size_t n;
+	int sockfd;
+	int testval;
+	int ign=0;
+
+	/*
+	 * we need authentication setup, data to submit in our queue
+	 * and the handshake must have taken place already. abort otherwise.
+	 */
+	as_lock();
+	testval = (as_authinfo.user[0] == '\0' || as_authinfo.pass[0] == '\0'
+		   || as_hsinfo.need_handshake == 1 || list_empty(&as_queue_head));
+	as_unlock();
+	if (testval)
+		return;
+
+	/*
+	 * creating the md5response described in the protocol v1.1 definition
+	 *   - create md5 checksum of $as_pass and covert it to hex
+	 *   - append the md5challenge received in handshake to the just created string
+	 *   - create md5 checksum of the concatenated string and convert it to hex.
+	 * That's it.
+	 */
+	as_lock();
+	md5_csum(as_authinfo.pass, strlen(as_authinfo.pass), digest);
+	as_digest2hex(tmp, digest);
+	strncpy(tmp+32, as_hsinfo.challenge.s, 32);
+	tmp[64] = '\0';
+	md5_csum(tmp, 64, digest);
+	as_digest2hex(md5response, digest);
+
+	str_init(&msg_header);
+	str_init(&msg_content);
+	str_init(&stmp);
+	str_init(&msg);
+	/*
+	 * assemble our payload
+	 */
+	str_ncat(&msg_content, "u=",    2); str_scat(&msg_content, as_authinfo.user);
+	str_ncat(&msg_content, "&s=",   3); str_scat(&msg_content, md5response);
+	list_for_each_entry(entry, &as_queue_head, node) {
+		if (ign && entry->sc == AS_SC_DO_SUBMIT) {
+			entry->num-=ign;
+			entry->needrenumber=1;
+		}
+		if (entry->needrenumber) {
+			as_renumberentry(&(entry->s), entry->num);
+			entry->needrenumber=0;
+		}
+		if (entry->sc == AS_SC_DO_SUBMIT) {
+			str_cat(&msg_content, &(entry->s));
+			entry->sc = AS_SC_SUBMITTED;
+			++submissions;
+			d_print("scrobbling: \"%s\"\n", entry->s.s);
+		}
+		else {
+			++ign;
+			d_print("ignoring: \"%s\"\n", entry->s.s);
+		}
+	}
+	if (!submissions) {
+		/* no submissions, eh? */
+		as_unlock();
+		goto cleanup;
+	}
+	as_uint2string(msg_content.len, i2s); /* note the length of our payload for later use */
+	str_ncat(&msg_content, "\n", 1);
+
+	/* create http/1.1 header for our submission */
+	str_ncat(&msg_header, "POST ", 5);  str_cat(&msg_header, &(as_hsinfo.path)); str_ncat(&msg_header, " HTTP/1.1\r\n", 11);
+	str_ncat(&msg_header, "Host: ", 6); str_cat(&msg_header, &(as_hsinfo.host)); str_ncat(&msg_header, "\r\n", 2);
+	str_ncat(&msg_header, AS_UASTRING, AS_UASTRINGLEN);
+	str_ncat(&msg_header, "Pragma: no-cache", 16);
+	str_ncat(&msg_header, "\r\nContent-Type: application/x-www-form-urlencoded", 49);
+	str_ncat(&msg_header, "\r\nContent-Length: ", 18); str_scat(&msg_header, i2s);
+	str_ncat(&msg_header, "\r\nConnection: CLOSE\r\n\r\n", 23);
+
+	/* turn payload's special chars to utf8 */
+	/*str_lat1_to_utf8(&msg, &msg_content);
+	str_cpy(&msg_content, &msg);*/
+
+#ifdef AS_HEAVY_DEBUG
+	d_print("POSTING: \"%s%s\"\n", msg_header.s, msg_content.s);
+#endif
+
+	as_unlock();
+
+	/*
+	 * connect to the server we got in the handshake
+	 */
+	if ( (sockfd = tcp_connect(as_hsinfo.host.s, as_hsinfo.port)) < 0 ) {
+		d_print("tcp_connect() failed!\n");
+		as_lock();
+		as_hsinfo.failed++;
+		as_unlock();
+		goto cleanup;
+	}
+
+	/*
+	 * send our submission to the server
+	 */
+	str_cpy(&msg, &msg_header);
+	str_cat(&msg, &msg_content);
+	xwriten(sockfd, msg.s, msg.len);
+	str_free(&msg);
+
+	msg_content.s[0] = '\0';
+	msg_content.len  = 0;
+
+	/* read response to our submission */
+	for (;;) {
+		if ( (n = xreadn(sockfd, buf, AS_BUFSIZE) ) <= 0)
+			break;
+		buf[n] = '\0';
+		str_ncat(&msg_content, buf, n);
+	}
+	xclose(sockfd);
+
+#ifdef AS_HEAVY_DEBUG
+	d_print("Serverreply:\n\"%s\"\n", msg_content.s);
+#endif
+
+	/* parse the response */
+	as_lock();
+	if (str_ngetidx(&msg_content, 0, "\r\n\r\n", 4, &idx)) {
+		str_nreplace(&msg_content, 0, idx+4, "", 0);
+	}
+	else {
+		d_print("broken reply, couldn't find end of headers (\\r\\n\\r\\n)\n");
+		as_hsinfo.failed++;
+		as_unlock();
+		goto cleanup;
+	}
+	odx=0;
+	if (!str_ngetidx(&msg_content, odx, "\n", 1, &idx)) {
+		d_print("Broken input: \"%s\"\n", msg_content.s);
+		as_hsinfo.failed++;
+		as_unlock();
+		goto cleanup;
+	}
+	str_ncpy(&stmp, msg_content.s+odx, idx-odx);
+
+	/* OK/FAILED/BADAUTH */
+	if      (str_ngetidx(&stmp, 0, "OK", 2, NULL)) {
+		d_print("received an OK, good.\n");
+	}
+	else if (str_ngetidx(&stmp, 0, "FAILED", 6, NULL)) {
+		stmp.s[idx] = '\0';
+		d_print("received FAILED (%s)\n", stmp.s+odx);
+		as_hsinfo.failed++;
+		as_unlock();
+		goto cleanup;
+	}
+	else if (str_ngetidx(&stmp, 0, "BADAUTH", 7, NULL)) {
+		d_print("received BADUSER, err, check as_user setting.\n");
+		as_hsinfo.failed++;
+		as_unlock();
+		goto cleanup;
+	}
+	else {
+		d_print("Unknown response: \"%s\"\n", stmp.s);
+		as_hsinfo.failed++;
+		as_unlock();
+		goto cleanup;
+	}
+
+	/* INTERVAL is optional here. */
+	odx=idx+1;
+	if (str_ngetidx(&msg_content, odx, "\n", 1, &idx)) {
+		str_ncpy(&stmp, msg_content.s+odx, idx-odx);
+		if (str_ngetidx(&stmp, 0, "INTERVAL ", 9, NULL)) {
+			as_hsinfo.interval=atoi(stmp.s+9);
+			d_print("Our Interval is: %d\n", (int)as_hsinfo.interval);
+		}
+	}
+	as_unlock();
+
+	/*
+	 * submission suceeded
+	 * destroy the part of the queue, that holds these
+	 */
+	as_destroy_queue(AS_SC_SUBMITTED);
+cleanup:
+	if (as_hsinfo.failed) {
+		/* resetting AS_SC_SUBMITTED, because something went wrong */
+		list_for_each_entry(entry, &as_queue_head, node) {
+			if (entry->sc == AS_SC_SUBMITTED)
+				entry->sc = AS_SC_DO_SUBMIT;
+		}
+	}
+	str_free(&msg);
+	str_free(&stmp);
+	str_free(&msg_header);
+	str_free(&msg_content);
+} /*}}}*/
+
+static void as_gettimestring(char *str, int maxlen)
+{ /*{{{*/
+	const time_t now = time(NULL);
+	const struct tm *timeinfo = gmtime(&now);
+	strftime(str, maxlen, "%Y%%2d%m%%2d%d%%20%H%%3a%M%%3a%S", timeinfo);
+} /*}}}*/
+
+static void as_add_queue(string_t *trackartist,
+			 string_t *trackalbum,
+			 string_t *trackname,
+			 int tracklen, int submissioncode)
+{ /*{{{*/
+	/*
+	 * Description:
+	 *  This adds a track to the submission-queue.
+	 *  Only 10 Tracks are allowed to be submitted at once.
+	 *  So, if we run past this limit, delete this first item of the list
+	 *  and append this track to the end of the list.
+	 */
+	struct as_queue *ptr, *tmp;
+	char tmpstr[AS_MAXTMPLEN+1];
+	int tmplen;
+	int queue_size;
+
+	d_print("adding track to as_queue:\n");
+	d_print("     Artist: %s\n",  trackartist->s);
+	d_print("     Album : %s\n",  trackalbum->s);
+	d_print("     Name  : %s\n",  trackname->s);
+	d_print("     Length: %d\n",  tracklen);
+
+	if (!(ptr=malloc(sizeof(struct as_queue)))) {
+		d_print("uark! out of memory? I give up.");
+		return;
+	}
+
+	as_lock();
+
+	if (list_empty(&as_queue_head)) {
+		ptr->num=0;
+		list_add_tail(&(ptr->node), &as_queue_head);
+	}
+	else {
+		/* let's find our place in the queue */
+		queue_size=as_count_entries();
+		tmp = as_get_last_entry();
+		if (queue_size >= AS_QUEUE_MAX) {
+			as_del_queue(as_queue_head.next);
+			ptr->num=AS_QUEUE_MAX;
+			list_add_tail(&(ptr->node), &as_queue_head);
+			list_for_each_entry(tmp, &as_queue_head, node) {
+				tmp->num--;
+				tmp->needrenumber=1;
+			}
+		}
+		else {
+			list_add_tail(&(ptr->node), &as_queue_head);
+			ptr->num=queue_size;
+		}
+	}
+
+	str_init(&(ptr->s));
+	ptr->sc=submissioncode;
+	ptr->needrenumber=0;
+	ptr->len=tracklen;
+	/* set up the request string */
+	as_urlencode(trackartist);
+	as_urlencode(trackalbum);
+	as_urlencode(trackname);
+	as_addtag(ptr->s, trackartist, "&a[");
+	as_addtag(ptr->s, trackname,   "&t[");
+	as_addtag(ptr->s, trackalbum,  "&b[");
+	/*
+	 * MusicBrainz ID, I'm just sending an empty one, because the
+	 * protocol definition says "don't skip it"
+	 */
+	str_readya(&(ptr->s), 6);
+	str_ncat(  &(ptr->s), "&m[", 3);
+	str_ncat(  &(ptr->s), as_numbers[ptr->num], 1);
+	str_ncat(  &(ptr->s), "]=", 2);
+	tmplen = as_uint2string(ptr->len, tmpstr);
+	as_addntag(ptr->s, tmpstr, tmplen, "&l[");
+	as_gettimestring(tmpstr, AS_MAXTMPLEN);
+	as_addntag(ptr->s, tmpstr, AS_MAXTMPLEN-1, "&i[");
+	as_unlock();
+} /*}}}*/
+
+void as_hook(enum as_actions action)
+{ /*{{{*/
+	/*
+	 * When do we need to call as_hook()?
+	 *  + stop
+	 *  + seek
+	 *  + next
+	 *  + prev
+	 *  + automatic song change
+	 *
+	 * What do we need to know?
+	 *  + track length
+	 *  + track position (we query player_info for that)
+	 *  + the action that called us
+	 *  + track name
+	 *  + artist
+	 *  + album
+	 */
+	static int lastwasinvalid   = 0; /* these 2 static ints are there */
+	static int alreadysubmitted = 0; /* to handle AS_SEEK properly.   */
+	int trackpos, tracklen;
+	int testval;
+	const char *tmp;
+	struct track_info *ti;
+	string_t trackartist, trackalbum, trackname;
+
+#ifdef AS_HEAVY_DEBUG
+	d_print("-- ping -----------------HOOOOK!1!!-------------------------------------\n");
+#endif
+
+	as_lock();
+	testval=running;
+	as_unlock();
+	if (!testval)
+		return;
+
+	if (lastwasinvalid) {
+		/*
+		 * last was invalid; if something else than SEEK
+		 * was done, this would submit the invalid track, so we
+		 * reset lastwasinvalid here and return.
+		 *
+		 * if SEEK was invoked again, we just return, because
+		 * the track was marked as invalid before.
+		 */
+		if (action != AS_SEEK)
+			lastwasinvalid = 0;
+		return;
+	}
+	if (alreadysubmitted) {
+		if (action != AS_SEEK)
+			alreadysubmitted = 0;
+		return;
+	}
+
+	player_info_lock();
+	trackpos=player_info.pos;
+	ti=cmus_get_track_info(player_info.filename);
+
+	if (ti == NULL) {
+		player_info_unlock();
+		return;
+	}
+
+	tracklen=ti->duration;
+
+	if (tracklen < AS_TRACK_MINLEN) { /* track is not long enough */
+		player_info_unlock();
+		return;
+	}
+	if (trackpos < AS_TRACK_MINSECS && (tracklen/2 > trackpos)) { /* track did not play long enough */
+		/*
+		 * if we were called in NEXT, PREV, STOP or *_ENTER
+		 * we already did skip to the next track (or stopped),
+		 * which means, we don't need to save this one for later review.
+		 * So, we return.
+		 */
+		player_info_unlock();
+		if (action != AS_SEEK)
+			return;
+
+		lastwasinvalid = 1;
+		return;
+	}
+
+	str_init(&trackartist);
+	str_init(&trackalbum);
+	str_init(&trackname);
+	tmp=comments_get_val(ti->comments, "artist");
+	str_scpy(&trackartist, tmp);
+	tmp=comments_get_val(ti->comments, "album");
+	str_scpy(&trackalbum, tmp);
+	tmp=comments_get_val(ti->comments, "title");
+	str_scpy(&trackname, tmp);
+	player_info_unlock();
+
+	if (trackartist.len == 0 || trackalbum.len == 0 || trackname.len == 0) {
+		/* One of the tags we must supply is empty */
+		return;
+	}
+
+	as_add_queue(&trackartist, &trackalbum, &trackname, tracklen, AS_SC_DO_SUBMIT);
+
+	if (action == AS_SEEK)
+		alreadysubmitted = 1;
+
+	str_free(&trackartist);
+	str_free(&trackalbum);
+	str_free(&trackname);
+} /*}}}*/
+
+static void as_sleep(void)
+{ /*{{{*/
+	struct timespec r;
+	r.tv_sec=AS_SLEEPSEC;
+	r.tv_nsec=AS_SLEEPNANO;
+	nanosleep(&r, NULL);
+} /*}}}*/
+
+void *as_loop(void *arg)
+{ /*{{{*/
+	/*
+	 * main audio scrobbler loop
+	 *  Here, all communication is done.
+	 *  Needed information is provided by *as_queue,
+	 *  which is filled by as_hook()
+	 *
+	 *  TODO: AS_SC_DONT_SUBMIT cases must be handled (eg. the user used seek on a track before it fitted our needs).
+	 */
+	static int last_as_state=0;
+	int testval;
+	struct as_queue *e;
+
+	for (;;) {
+#ifdef AS_HEAVY_DEBUG
+		d_print("-- ping ----------------------------------------------------------------\n");
+#endif
+		as_update_timestamp();
+		as_lock();
+		testval = list_empty(&as_queue_head);
+		as_unlock();
+		if (testval) {
+			/* nothing to submit */
+			if (!running) {
+				/*
+				 * running isn't set,
+				 * seems like the user switched us off
+				 */
+				return(NULL);
+			}
+			as_sleep();
+		}
+		else {
+			as_lock();
+			list_for_each_entry(e, &as_queue_head, node) {
+				d_print("--DEBUG--\n");
+				d_print("     Number: %d\n",   e->num);
+				d_print("     SCode : %d\n",   e->sc);
+				d_print("     Length: %d\n",   e->len);
+				d_print("     NEEDRN: %d\n",   e->needrenumber);
+				d_print("     string: %s\n\n", e->s.s);
+			}
+			as_unlock();
+			as_sleep();
+			as_lock();
+			testval = as_hsinfo.need_handshake;
+			as_unlock();
+			if (testval) {
+				as_handshake();
+			}
+			as_lock();
+			testval = as_hsinfo.need_handshake;
+			as_unlock();
+			if (!testval) {
+				/*
+				 * we are enabled, got data to submit and don't need to handshake.
+				 * check, if we may (or need to force to) submit our data,
+				 * and do so, if needed.
+				 *
+				 * XXX: as_submit_allowed() and as_submit_forced() do locking on their own.
+				 */
+				if (as_submit_allowed() || as_submit_forced()) {
+					as_submit();
+				}
+			}
+		}
+
+		as_lock();
+		if (!as_enable && !last_as_state) {
+			/*
+			 * user didn't enable us in the first place.
+			 * so, set running to 0 and rerun loop, so we can exit
+			 */
+			running=0;
+		}
+		else if (!as_enable && last_as_state) {
+			/* as_enable was on before, so terminate session */
+			running=0;
+			last_as_state=0;
+		}
+		else if (!last_as_state && as_enable) {
+			/* (re)start as session */
+			running=1;
+			as_hsinfo.need_handshake=1;
+			last_as_state=1;
+		}
+		as_unlock();
+	}
+} /*}}}*/
+
+void as_init(void)
+{ /*{{{*/
+	int rc;
+	as_lock();
+	running=1;
+	str_init(&(as_hsinfo.challenge));
+	str_init(&(as_hsinfo.host));
+	str_init(&(as_hsinfo.path));
+	as_hsinfo.need_handshake = 1;
+	as_hsinfo.interval = 0;
+	as_hsinfo.failed = 0;
+	as_lasttimestamp = 0;
+	as_unlock();
+	rc = pthread_create(&as_thread, NULL, as_loop, NULL);
+	BUG_ON(rc);
+} /*}}}*/
+
+void as_exit(void)
+{ /*{{{*/
+	as_lock();
+	running = 0;
+	as_destroy_queue(AS_SC_ALL);
+	str_free(&(as_hsinfo.challenge));
+	str_free(&(as_hsinfo.host));
+	str_free(&(as_hsinfo.path));
+	as_unlock();
+	pthread_join(as_thread, NULL);
+} /*}}}*/
diff --git a/as.h b/as.h
new file mode 100644
index 0000000..40b2ebd
--- /dev/null
+++ b/as.h
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2004-2006 Timo Hirvonen
+ *
+ * as.[ch] by Frank Terbeck <ft@bewatermyfriend.de>
+ *
+ * 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., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ */
+
+#ifndef x__AS_H
+#define x__AS_H
+
+/*
+ * audioscrobbler code for cmus {{{
+ *   (read: get cmus properly working with http://last.fm/
+ *
+ * When are songs submitted?
+ *  + Each song should be posted to the server when it is 50%
+ *    or 240 seconds complete, whichever comes first.
+ *
+ *  + If a user seeks (i.e. manually changes position) within a song
+ *    before the song is due to be submitted, do not submit that song.
+ *
+ *  + Songs with a duration of less than 30 seconds should not be
+ *    submitted (so, we don't do that).
+ *
+ *  + Do not try to guess info from the filename - if there are no
+ *    tags, don't submit the file (okeydoke, herman).
+ *
+ *  + If a MusicBrainz ID is present in the file (as defined here),
+ *    then you should send it (whatever that is...[1]).
+ *
+ *  + If a user is playing a stream instead of a regular file, do
+ *    not submit that stream/song.
+ *
+ *********************************************************************
+ *
+ * [1] About musicbrainz.org:
+ *      I don't know what that is, nor do I care. Feel free to
+ *      implement support for it, if you need it desperately.
+ *}}}
+ */
+
+#include "list.h"
+#include "options.h"
+#include "strlib.h"
+#include "track_info.h"
+
+/* #defines */
+
+/* We are currently testing things, so "tst" is okay. */
+#define AS_UASTRING        "User-Agent: AudioScrobbler/1.1 cmus-scrobbler code v0.01\r\n"
+#define AS_UASTRINGLEN       58
+#define AS_CLIENT_ID       "tst"
+#define AS_PROTO           "1.1"
+
+#define AS_HOSTNAME        "post.audioscrobbler.com"
+#define AS_HOSTPORT        "80"
+#define AS_HS_URI          "/?hs=true"
+
+#define AS_BUFSIZE         4096
+
+/* AS_QUEUE_MAX must of an element of [1..10] */
+#define AS_QUEUE_MAX         10
+
+/*
+ * AS_FORCE_SUBMIT:
+ * if our queue holds this many entries, force submission,
+ * no matter what the last INTERVAL response said.
+ */
+#define AS_FORCE_SUBMIT       8
+
+/* AS_DO_SUBMIT marks a track for submission */
+#define AS_SC_DO_SUBMIT       1
+
+/* AS_DONT_SUBMIT is used if seek was used at the beginning of a track */
+#define AS_SC_DONT_SUBMIT     2
+
+/* AS_SC_SUBMITTED marks tracks, that where included in a submission msg */
+#define AS_SC_SUBMITTED       4
+
+/* AS_SC_ALL is the same as ORing together all other AS_SC_* macros */
+#define AS_SC_ALL            15
+
+/* The minimum length of a track to be considered for submission */
+#define AS_TRACK_MINLEN      30
+
+/* A track must have run 50% of its time or AS_TRACK_MINSECS seconds */
+#define AS_TRACK_MINSECS    240
+
+/* AS_SLEEP* sets up the resolution of as_loop() */
+#define AS_SLEEPSEC           5
+#define AS_SLEEPNANO          0
+
+/*
+ * for temporary static strings, that need to be long enough
+ * to hold an url_encoded time string (25+1); this is used for
+ * strings that are converted integers.
+ */
+#define AS_MAXTMPLEN         30
+
+/* data types */
+enum as_actions {
+	AS_AUTONEXT=1,
+	AS_SEEK,
+	AS_NEXT,
+	AS_PREV,
+	AS_STOP,
+	AS_TREE_ENTER,
+	AS_SORTED_ENTER,
+	AS_PL_ENTER
+};
+
+struct as_queue {
+	struct list_head node;
+	string_t s;
+	unsigned int len;
+	unsigned short int sc;           /* AS_SC_* */
+	unsigned short int needrenumber; /* we must renumber 'string_t s'  */
+	unsigned short int num;          /* number of the current entry    */
+};
+
+struct as_hsinfo {
+	unsigned int need_handshake;
+	unsigned int failed;
+	time_t       interval;
+	string_t challenge;
+	string_t host;
+	string_t path;
+	char     port[AS_MAXTMPLEN+1];   /* yes, port as a string. tcp_connect expects its port as char[] */
+};
+
+struct as_authinfo {
+	char user[OPTION_MAX_SIZE];
+	char pass[OPTION_MAX_SIZE];
+};
+
+/* function prototypes */
+void as_hook(enum as_actions action);
+void *as_loop(void *arg);
+void as_exit(void);
+void as_init(void);
+
+/* macros */
+#define as_addtag(dest, src, tag)			\
+	str_readya(&(dest), 6);				\
+	str_ncat(  &(dest), tag, 3);			\
+	str_ncat(  &(dest), as_numbers[ptr->num], 1);	\
+	str_ncat(  &(dest), "]=", 2);			\
+	str_readya(&(dest), src->len);			\
+	str_cat(   &(dest), src)
+
+#define as_addntag(dest, src, len, tag)			\
+	str_readya(&(dest), 6);				\
+	str_ncat(  &(dest), tag, 3);			\
+	str_ncat(  &(dest), as_numbers[ptr->num], 1);	\
+	str_ncat(  &(dest), "]=", 2);			\
+	str_readya(&(dest), len);			\
+	str_ncat(  &(dest), src, len)
+
+#define mustquote(c) \
+	c == ' '  || \
+	c == '!'  || \
+	c == '\"' || \
+	c == '#'  || \
+	c == '%'  || \
+	c == '&'  || \
+	c == '\'' || \
+	c == '('  || \
+	c == ')'  || \
+	c == '*'  || \
+	c == '-'  || \
+	c == '/'  || \
+	c == ':'  || \
+	c == ';'  || \
+	c == '<'  || \
+	c == '='  || \
+	c == '>'  || \
+	c == '?'  || \
+	c == '@'  || \
+	c == '['  || \
+	c == '\\' || \
+	c == ']'  || \
+	c == '^'  || \
+	c == '`'  || \
+	c == '{'  || \
+	c == '|'  || \
+	c == '}'  || \
+	c == '~'
+
+#define as_lock()   cmus_mutex_lock(&as_mutex)
+#define as_unlock() cmus_mutex_unlock(&as_mutex)
+
+/* exported variables */
+extern int as_enable;
+extern struct as_authinfo as_authinfo;
+
+#endif /* x__AS_H */
diff --git a/cmus.c b/cmus.c
index 50aafaf..55f1c53 100644
--- a/cmus.c
+++ b/cmus.c
@@ -14,6 +14,7 @@
 #include "xmalloc.h"
 #include "xstrjoin.h"
 #include "debug.h"
+#include "as.h"
 #include "load_dir.h"
 
 #include <sys/types.h>
diff --git a/command_mode.c b/command_mode.c
index 9626d24..4c94da0 100644
--- a/command_mode.c
+++ b/command_mode.c
@@ -45,6 +45,7 @@
 #include "list.h"
 #include "debug.h"
 #include "load_dir.h"
+#include "as.h"
 #include "config/datadir.h"
 #include "help.h"
 
@@ -454,6 +455,8 @@ static void cmd_seek(char *arg)
 	int relative = 0;
 	int seek = 0, sign = 1, count;
 
+	as_hook(AS_SEEK);
+
 	switch (*arg) {
 	case '-':
 		sign = -1;
@@ -1191,6 +1194,7 @@ static void cmd_view(char *arg)
 
 static void cmd_p_next(char *arg)
 {
+	as_hook(AS_NEXT);
 	cmus_next();
 }
 
@@ -1210,11 +1214,13 @@ static void cmd_p_play(char *arg)
 
 static void cmd_p_prev(char *arg)
 {
+	as_hook(AS_PREV);
 	cmus_prev();
 }
 
 static void cmd_p_stop(char *arg)
 {
+	as_hook(AS_STOP);
 	player_stop();
 }
 
@@ -1736,7 +1742,6 @@ static void expand_key_context(const char *str, const char *force)
 	snprintf(expbuf, sizeof(expbuf), "%s%s", force, str);
 	tabexp.head = xstrdup(expbuf);
 	tabexp.tails = tails;
-	tabexp.nr_tails = pos;
 }
 
 static int get_context(const char *str, int len)
@@ -1832,7 +1837,6 @@ static void expand_bind_args(const char *str)
 		ptr_array_plug(&array);
 		tabexp.head = xstrdup(expbuf);
 		tabexp.tails = array.ptrs;
-		tabexp.nr_tails = array.count;
 		return;
 	}
 
@@ -1926,7 +1930,6 @@ static void expand_unbind_args(const char *str)
 	ptr_array_plug(&array);
 	tabexp.head = xstrdup(expbuf);
 	tabexp.tails = array.ptrs;
-	tabexp.nr_tails = array.count;
 }
 
 static void expand_factivate(const char *str)
@@ -1957,7 +1960,6 @@ static void expand_factivate(const char *str)
 	ptr_array_plug(&array);
 	tabexp.head = xstrdup(str);
 	tabexp.tails = array.ptrs;
-	tabexp.nr_tails = array.count;
 }
 
 static void expand_options(const char *str)
@@ -1985,7 +1987,6 @@ static void expand_options(const char *str)
 
 				tabexp.head = xstrdup(str);
 				tabexp.tails = tails;
-				tabexp.nr_tails = 1;
 				free(var);
 				return;
 			}
@@ -2013,7 +2014,6 @@ static void expand_options(const char *str)
 			tails[pos] = NULL;
 			tabexp.head = xstrdup(str);
 			tabexp.tails = tails;
-			tabexp.nr_tails = pos;
 		} else {
 			free(tails);
 		}
@@ -2039,7 +2039,6 @@ static void expand_toptions(const char *str)
 		tails[pos] = NULL;
 		tabexp.head = xstrdup(str);
 		tabexp.tails = tails;
-		tabexp.nr_tails = pos;
 	} else {
 		free(tails);
 	}
@@ -2086,7 +2085,6 @@ static void expand_colorscheme(const char *str)
 		ptr_array_plug(&array);
 		tabexp.head = xstrdup(str);
 		tabexp.tails = array.ptrs;
-		tabexp.nr_tails = array.count;
 	}
 }
 
@@ -2174,7 +2172,6 @@ static void expand_commands(const char *str)
 		tails[pos] = NULL;
 		tabexp.head = xstrdup(str);
 		tabexp.tails = tails;
-		tabexp.nr_tails = pos;
 	} else {
 		free(tails);
 	}
diff --git a/comment.c b/comment.c
index 0cf2cb6..c36c705 100644
--- a/comment.c
+++ b/comment.c
@@ -75,6 +75,43 @@ int comments_get_int(const struct keyval *comments, const char *key)
 	return ival;
 }
 
+/* Return date as an integer in the form YYYYMMDD, for sorting purposes.
+ * This function is not year 10000 compliant. */
+int comments_get_date(const struct keyval *comments, const char *key)
+{
+	const char *val;
+	char *endptr;
+	int year, month, day;
+	long int ival;
+
+	val = comments_get_val(comments, key);
+	if (val == NULL)
+		return -1;
+
+	year = strtol(val, &endptr, 10);
+	/* Looking for a four-digit number */
+	if (year < 1000 || year > 9999)
+		return -1;
+	ival = year * 10000;
+
+	if (*endptr == '-' || *endptr == ' ' || *endptr == '/') {
+		month = strtol(endptr+1, &endptr, 10);
+		if (month < 1 || month > 12)
+			return ival;
+		ival += month * 100;
+	}
+
+	if (*endptr == '-' || *endptr == ' ' || *endptr == '/') {
+		day = strtol(endptr+1, &endptr, 10);
+		if (day < 1 || day > 31)
+			return ival;
+		ival += day;
+	}
+
+
+	return ival;
+}
+
 static const char *interesting[] = {
 	"artist", "album", "title", "tracknumber", "discnumber", "genre",
 	"date", "compilation", "albumartist",  NULL
diff --git a/comment.h b/comment.h
index 0e5b991..193fc9e 100644
--- a/comment.h
+++ b/comment.h
@@ -31,6 +31,7 @@ extern void comments_free(struct keyval *comments);
 /* case insensitive key */
 extern const char *comments_get_val(const struct keyval *comments, const char *key);
 extern int comments_get_int(const struct keyval *comments, const char *key);
+extern int comments_get_date(const struct keyval *comments, const char *key);
 
 int is_interesting_key(const char *key);
 void fix_track_or_disc(char *str);
diff --git a/configure b/configure
index 40750de..d1dce2d 100755
--- a/configure
+++ b/configure
@@ -33,6 +33,10 @@ check_compat()
 		then
 			COMPAT_LIBS="$COMPAT_LIBS -lposix4"
 		fi
+		;;
+	CYGWIN*)
+		CONFIG_CYGWIN=y
+		makefile_vars CONFIG_CYGWIN
 	esac
 	makefile_vars COMPAT_LIBS
 }
@@ -158,6 +162,16 @@ check_sun()
 	fi
 }
 
+check_waveout()
+{
+	case `uname -s` in
+	CYGWIN*)
+		check_library WAVEOUT "" "-lwinmm"
+		return $?
+	esac
+	return 1
+}
+
 check_mp4()
 {
 	check_header neaacdec.h &&
@@ -204,6 +218,7 @@ Optional Features: y/n
   CONFIG_ARTS     ARTS                                            [auto]
   CONFIG_OSS      Open Sound System                               [auto]
   CONFIG_SUN      Sun Audio                                       [auto]
+  CONFIG_WAVEOUT  Windows Wave Out                                [auto]
 
 Also many standard variables like CC are recognized."
 
@@ -248,6 +263,7 @@ check check_ao      CONFIG_AO
 check check_arts    CONFIG_ARTS
 check check_oss     CONFIG_OSS
 check check_sun     CONFIG_SUN
+check check_waveout CONFIG_WAVEOUT
 
 test "$WORDS_BIGENDIAN" = y && CFLAGS="${CFLAGS} -DWORDS_BIGENDIAN"
 
@@ -261,6 +277,6 @@ config_header config/tremor.h CONFIG_TREMOR
 
 makefile_vars bindir datadir libdir mandir
 makefile_vars CONFIG_FLAC CONFIG_MAD CONFIG_MIKMOD CONFIG_MODPLUG CONFIG_MPC CONFIG_VORBIS CONFIG_WAV CONFIG_MP4 CONFIG_AAC
-makefile_vars CONFIG_ALSA CONFIG_AO CONFIG_ARTS CONFIG_OSS CONFIG_SUN
+makefile_vars CONFIG_ALSA CONFIG_AO CONFIG_ARTS CONFIG_OSS CONFIG_SUN CONFIG_WAVEOUT
 
 generate_config_mk
diff --git a/editable.c b/editable.c
index ae0dce0..3394b87 100644
--- a/editable.c
+++ b/editable.c
@@ -105,7 +105,10 @@ static const char **sort_keys;
 
 static int list_cmp(const struct list_head *a_head, const struct list_head *b_head)
 {
-	return simple_track_cmp(a_head, b_head, sort_keys);
+	const struct simple_track *a = to_simple_track(a_head);
+	const struct simple_track *b = to_simple_track(b_head);
+
+	return track_info_cmp(a->info, b->info, sort_keys);
 }
 
 void editable_sort(struct editable *e)
diff --git a/flac.c b/flac.c
index 10b4762..a95a497 100644
--- a/flac.c
+++ b/flac.c
@@ -338,6 +338,10 @@ static void metadata_cb(const Dec *dec, const FLAC__StreamMetadata *metadata, vo
 							&key, &val))
 					continue;
 
+				if (!strcasecmp(key, "album_artist")) {
+					free(key);
+					key = xstrdup("albumartist");
+				}
 				if (!is_interesting_key(key)) {
 					free(key);
 					free(val);
diff --git a/lib.c b/lib.c
index 4206b46..6204585 100644
--- a/lib.c
+++ b/lib.c
@@ -4,9 +4,11 @@
 
 #include "lib.h"
 #include "editable.h"
+#include "track_info.h"
 #include "options.h"
 #include "xmalloc.h"
 #include "debug.h"
+#include "as.h"
 
 #include <pthread.h>
 #include <string.h>
@@ -357,6 +359,7 @@ struct track_info *sorted_set_selected(void)
 	if (list_empty(&lib_editable.head))
 		return NULL;
 
+	as_hook(AS_SORTED_ENTER);
 	window_get_sel(lib_editable.win, &sel);
 	return lib_set_track(iter_to_sorted_track(&sel));
 }
@@ -462,12 +465,12 @@ void sorted_sel_current(void)
 	}
 }
 
-static int ti_filename_cmp(const void *a, const void *b)
+static int ti_cmp(const void *a, const void *b)
 {
-	const struct track_info *tia = *(const struct track_info **)a;
-	const struct track_info *tib = *(const struct track_info **)b;
+	const struct track_info *ai = *(const struct track_info **)a;
+	const struct track_info *bi = *(const struct track_info **)b;
 
-	return strcmp(tia->filename, tib->filename);
+	return track_info_cmp(ai, bi, lib_editable.sort_keys);
 }
 
 int lib_for_each(int (*cb)(void *data, struct track_info *ti), void *data)
@@ -492,8 +495,8 @@ int lib_for_each(int (*cb)(void *data, struct track_info *ti), void *data)
 		}
 	}
 
-	/* sort them by filename and call cb for each */
-	qsort(tis, count, sizeof(struct track_info *), ti_filename_cmp);
+	/* sort to speed up playlist loading */
+	qsort(tis, count, sizeof(struct track_info *), ti_cmp);
 	for (i = 0; i < count; i++) {
 		rc = cb(data, tis[i]);
 		if (rc)
diff --git a/load_dir.c b/load_dir.c
index 5426fc0..6d5a0ee 100644
--- a/load_dir.c
+++ b/load_dir.c
@@ -43,6 +43,14 @@ const char *dir_read(struct directory *dir)
 	char *full = dir->path;
 	struct dirent *de;
 
+#if defined(__CYGWIN__)
+	/* Fix for cygwin "hang" bug when browsing /
+	 * Windows treats // as a network path.
+	 */
+	if (strcmp(full, "//") == 0)
+		full++;
+#endif
+
 	while ((de = readdir(d))) {
 		const char *name = de->d_name;
 		int nlen = strlen(name);
diff --git a/md5.c b/md5.c
new file mode 100644
index 0000000..5b31899
--- /dev/null
+++ b/md5.c
@@ -0,0 +1,408 @@
+/*
+ *  RFC 1321 compliant MD5 implementation
+ *
+ *  Copyright (C) 2003-2006  Christophe Devine
+ *
+ *  This library 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.
+ *
+ *  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
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *  MA  02110-1301  USA
+ */
+/*
+ *  The MD5 algorithm was designed by Ron Rivest in 1991.
+ *
+ *  http://www.ietf.org/rfc/rfc1321.txt
+ */
+
+#ifndef _CRT_SECURE_NO_DEPRECATE
+#define _CRT_SECURE_NO_DEPRECATE 1
+#endif
+
+#include <string.h>
+#include <stdio.h>
+
+#include "md5.h"
+
+/* 
+ * 32-bit integer manipulation macros (little endian)
+ */
+#ifndef GET_UINT32_LE
+#define GET_UINT32_LE(n,b,i)                    \
+{                                               \
+    (n) = ( (ulong) (b)[(i)    ]       )        \
+        | ( (ulong) (b)[(i) + 1] <<  8 )        \
+        | ( (ulong) (b)[(i) + 2] << 16 )        \
+        | ( (ulong) (b)[(i) + 3] << 24 );       \
+}
+#endif
+#ifndef PUT_UINT32_LE
+#define PUT_UINT32_LE(n,b,i)                    \
+{                                               \
+    (b)[(i)    ] = (uchar) ( (n)       );       \
+    (b)[(i) + 1] = (uchar) ( (n) >>  8 );       \
+    (b)[(i) + 2] = (uchar) ( (n) >> 16 );       \
+    (b)[(i) + 3] = (uchar) ( (n) >> 24 );       \
+}
+#endif
+
+/*
+ * Core MD5 functions
+ */
+void md5_starts( md5_context *ctx )
+{
+    ctx->total[0] = 0;
+    ctx->total[1] = 0;
+
+    ctx->state[0] = 0x67452301;
+    ctx->state[1] = 0xEFCDAB89;
+    ctx->state[2] = 0x98BADCFE;
+    ctx->state[3] = 0x10325476;
+}
+
+static void md5_process( md5_context *ctx, uchar data[64] )
+{
+    ulong X[16], A, B, C, D;
+
+    GET_UINT32_LE( X[0],  data,  0 );
+    GET_UINT32_LE( X[1],  data,  4 );
+    GET_UINT32_LE( X[2],  data,  8 );
+    GET_UINT32_LE( X[3],  data, 12 );
+    GET_UINT32_LE( X[4],  data, 16 );
+    GET_UINT32_LE( X[5],  data, 20 );
+    GET_UINT32_LE( X[6],  data, 24 );
+    GET_UINT32_LE( X[7],  data, 28 );
+    GET_UINT32_LE( X[8],  data, 32 );
+    GET_UINT32_LE( X[9],  data, 36 );
+    GET_UINT32_LE( X[10], data, 40 );
+    GET_UINT32_LE( X[11], data, 44 );
+    GET_UINT32_LE( X[12], data, 48 );
+    GET_UINT32_LE( X[13], data, 52 );
+    GET_UINT32_LE( X[14], data, 56 );
+    GET_UINT32_LE( X[15], data, 60 );
+
+#define S(x,n) ((x << n) | ((x & 0xFFFFFFFF) >> (32 - n)))
+
+#define P(a,b,c,d,k,s,t)                                \
+{                                                       \
+    a += F(b,c,d) + X[k] + t; a = S(a,s) + b;           \
+}
+
+    A = ctx->state[0];
+    B = ctx->state[1];
+    C = ctx->state[2];
+    D = ctx->state[3];
+
+#define F(x,y,z) (z ^ (x & (y ^ z)))
+
+    P( A, B, C, D,  0,  7, 0xD76AA478 );
+    P( D, A, B, C,  1, 12, 0xE8C7B756 );
+    P( C, D, A, B,  2, 17, 0x242070DB );
+    P( B, C, D, A,  3, 22, 0xC1BDCEEE );
+    P( A, B, C, D,  4,  7, 0xF57C0FAF );
+    P( D, A, B, C,  5, 12, 0x4787C62A );
+    P( C, D, A, B,  6, 17, 0xA8304613 );
+    P( B, C, D, A,  7, 22, 0xFD469501 );
+    P( A, B, C, D,  8,  7, 0x698098D8 );
+    P( D, A, B, C,  9, 12, 0x8B44F7AF );
+    P( C, D, A, B, 10, 17, 0xFFFF5BB1 );
+    P( B, C, D, A, 11, 22, 0x895CD7BE );
+    P( A, B, C, D, 12,  7, 0x6B901122 );
+    P( D, A, B, C, 13, 12, 0xFD987193 );
+    P( C, D, A, B, 14, 17, 0xA679438E );
+    P( B, C, D, A, 15, 22, 0x49B40821 );
+
+#undef F
+
+#define F(x,y,z) (y ^ (z & (x ^ y)))
+
+    P( A, B, C, D,  1,  5, 0xF61E2562 );
+    P( D, A, B, C,  6,  9, 0xC040B340 );
+    P( C, D, A, B, 11, 14, 0x265E5A51 );
+    P( B, C, D, A,  0, 20, 0xE9B6C7AA );
+    P( A, B, C, D,  5,  5, 0xD62F105D );
+    P( D, A, B, C, 10,  9, 0x02441453 );
+    P( C, D, A, B, 15, 14, 0xD8A1E681 );
+    P( B, C, D, A,  4, 20, 0xE7D3FBC8 );
+    P( A, B, C, D,  9,  5, 0x21E1CDE6 );
+    P( D, A, B, C, 14,  9, 0xC33707D6 );
+    P( C, D, A, B,  3, 14, 0xF4D50D87 );
+    P( B, C, D, A,  8, 20, 0x455A14ED );
+    P( A, B, C, D, 13,  5, 0xA9E3E905 );
+    P( D, A, B, C,  2,  9, 0xFCEFA3F8 );
+    P( C, D, A, B,  7, 14, 0x676F02D9 );
+    P( B, C, D, A, 12, 20, 0x8D2A4C8A );
+
+#undef F
+    
+#define F(x,y,z) (x ^ y ^ z)
+
+    P( A, B, C, D,  5,  4, 0xFFFA3942 );
+    P( D, A, B, C,  8, 11, 0x8771F681 );
+    P( C, D, A, B, 11, 16, 0x6D9D6122 );
+    P( B, C, D, A, 14, 23, 0xFDE5380C );
+    P( A, B, C, D,  1,  4, 0xA4BEEA44 );
+    P( D, A, B, C,  4, 11, 0x4BDECFA9 );
+    P( C, D, A, B,  7, 16, 0xF6BB4B60 );
+    P( B, C, D, A, 10, 23, 0xBEBFBC70 );
+    P( A, B, C, D, 13,  4, 0x289B7EC6 );
+    P( D, A, B, C,  0, 11, 0xEAA127FA );
+    P( C, D, A, B,  3, 16, 0xD4EF3085 );
+    P( B, C, D, A,  6, 23, 0x04881D05 );
+    P( A, B, C, D,  9,  4, 0xD9D4D039 );
+    P( D, A, B, C, 12, 11, 0xE6DB99E5 );
+    P( C, D, A, B, 15, 16, 0x1FA27CF8 );
+    P( B, C, D, A,  2, 23, 0xC4AC5665 );
+
+#undef F
+
+#define F(x,y,z) (y ^ (x | ~z))
+
+    P( A, B, C, D,  0,  6, 0xF4292244 );
+    P( D, A, B, C,  7, 10, 0x432AFF97 );
+    P( C, D, A, B, 14, 15, 0xAB9423A7 );
+    P( B, C, D, A,  5, 21, 0xFC93A039 );
+    P( A, B, C, D, 12,  6, 0x655B59C3 );
+    P( D, A, B, C,  3, 10, 0x8F0CCC92 );
+    P( C, D, A, B, 10, 15, 0xFFEFF47D );
+    P( B, C, D, A,  1, 21, 0x85845DD1 );
+    P( A, B, C, D,  8,  6, 0x6FA87E4F );
+    P( D, A, B, C, 15, 10, 0xFE2CE6E0 );
+    P( C, D, A, B,  6, 15, 0xA3014314 );
+    P( B, C, D, A, 13, 21, 0x4E0811A1 );
+    P( A, B, C, D,  4,  6, 0xF7537E82 );
+    P( D, A, B, C, 11, 10, 0xBD3AF235 );
+    P( C, D, A, B,  2, 15, 0x2AD7D2BB );
+    P( B, C, D, A,  9, 21, 0xEB86D391 );
+
+#undef F
+
+    ctx->state[0] += A;
+    ctx->state[1] += B;
+    ctx->state[2] += C;
+    ctx->state[3] += D;
+}
+
+void md5_update( md5_context *ctx, uchar *input, uint length )
+{
+    ulong left, fill;
+
+    if( ! length ) return;
+
+    left = ctx->total[0] & 0x3F;
+    fill = 64 - left;
+
+    ctx->total[0] += length;
+    ctx->total[0] &= 0xFFFFFFFF;
+
+    if( ctx->total[0] < length )
+        ctx->total[1]++;
+
+    if( left && length >= fill )
+    {
+        memcpy( (void *) (ctx->buffer + left),
+                (void *) input, fill );
+        md5_process( ctx, ctx->buffer );
+        length -= fill;
+        input  += fill;
+        left = 0;
+    }
+
+    while( length >= 64 )
+    {
+        md5_process( ctx, input );
+        length -= 64;
+        input  += 64;
+    }
+
+    if( length )
+    {
+        memcpy( (void *) (ctx->buffer + left),
+                (void *) input, length );
+    }
+}
+
+static uchar md5_padding[64] =
+{
+ 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+};
+
+void md5_finish( md5_context *ctx, uchar digest[16] )
+{
+    ulong last, padn;
+    ulong high, low;
+    uchar msglen[8];
+
+    high = ( ctx->total[0] >> 29 )
+         | ( ctx->total[1] <<  3 );
+    low  = ( ctx->total[0] <<  3 );
+
+    PUT_UINT32_LE( low,  msglen, 0 );
+    PUT_UINT32_LE( high, msglen, 4 );
+
+    last = ctx->total[0] & 0x3F;
+    padn = ( last < 56 ) ? ( 56 - last ) : ( 120 - last );
+
+    md5_update( ctx, md5_padding, padn );
+    md5_update( ctx, msglen, 8 );
+
+    PUT_UINT32_LE( ctx->state[0], digest,  0 );
+    PUT_UINT32_LE( ctx->state[1], digest,  4 );
+    PUT_UINT32_LE( ctx->state[2], digest,  8 );
+    PUT_UINT32_LE( ctx->state[3], digest, 12 );
+}
+
+/*
+ * Output MD5(file contents), returns 0 if successful.
+ */
+int md5_file( char *filename, uchar digest[16] )
+{
+    FILE *f;
+    size_t n;
+    md5_context ctx;
+    uchar buf[1024];
+
+    if( ( f = fopen( filename, "rb" ) ) == NULL )
+        return( 1 );
+
+    md5_starts( &ctx );
+
+    while( ( n = fread( buf, 1, sizeof( buf ), f ) ) > 0 )
+        md5_update( &ctx, buf, (uint) n );
+
+    md5_finish( &ctx, digest );
+
+    fclose( f );
+    return( 0 );
+}
+
+/*
+ * Output MD5(buf)
+ */
+void md5_csum( uchar *buf, uint buflen, uchar digest[16] )
+{
+    md5_context ctx;
+
+    md5_starts( &ctx );
+    md5_update( &ctx, buf, buflen );
+    md5_finish( &ctx, digest );
+}
+
+/*
+ * Output HMAC-MD5(key,buf)
+ */
+void md5_hmac( uchar *key, uint keylen, uchar *buf, uint buflen,
+               uchar digest[16] )
+{
+    uint i;
+    md5_context ctx;
+    uchar k_ipad[64];
+    uchar k_opad[64];
+    uchar tmpbuf[16];
+
+    memset( k_ipad, 0x36, 64 );
+    memset( k_opad, 0x5C, 64 );
+
+    for( i = 0; i < keylen; i++ )
+    {
+        if( i >= 64 ) break;
+
+        k_ipad[i] ^= key[i];
+        k_opad[i] ^= key[i];
+    }
+
+    md5_starts( &ctx );
+    md5_update( &ctx, k_ipad, 64 );
+    md5_update( &ctx, buf, buflen );
+    md5_finish( &ctx, tmpbuf );
+
+    md5_starts( &ctx );
+    md5_update( &ctx, k_opad, 64 );
+    md5_update( &ctx, tmpbuf, 16 );
+    md5_finish( &ctx, digest );
+
+    memset( k_ipad, 0, 64 );
+    memset( k_opad, 0, 64 );
+    memset( tmpbuf, 0, 16 );
+    memset( &ctx, 0, sizeof( md5_context ) );
+}
+
+#ifdef SELF_TEST
+/* 
+ * RFC 1321 test vectors
+ */
+static char *md5_test_str[7] =
+{
+    "",
+    "a",
+    "abc",
+    "message digest",
+    "abcdefghijklmnopqrstuvwxyz",
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
+    "12345678901234567890123456789012345678901234567890123456789012" \
+        "345678901234567890"
+};
+
+static uchar md5_test_sum[7][16] =
+{
+    { 0xD4, 0x1D, 0x8C, 0xD9, 0x8F, 0x00, 0xB2, 0x04,
+      0xE9, 0x80, 0x09, 0x98, 0xEC, 0xF8, 0x42, 0x7E },
+    { 0x0C, 0xC1, 0x75, 0xB9, 0xC0, 0xF1, 0xB6, 0xA8,
+      0x31, 0xC3, 0x99, 0xE2, 0x69, 0x77, 0x26, 0x61 },
+    { 0x90, 0x01, 0x50, 0x98, 0x3C, 0xD2, 0x4F, 0xB0,
+      0xD6, 0x96, 0x3F, 0x7D, 0x28, 0xE1, 0x7F, 0x72 },
+    { 0xF9, 0x6B, 0x69, 0x7D, 0x7C, 0xB7, 0x93, 0x8D,
+      0x52, 0x5A, 0x2F, 0x31, 0xAA, 0xF1, 0x61, 0xD0 },
+    { 0xC3, 0xFC, 0xD3, 0xD7, 0x61, 0x92, 0xE4, 0x00,
+      0x7D, 0xFB, 0x49, 0x6C, 0xCA, 0x67, 0xE1, 0x3B },
+    { 0xD1, 0x74, 0xAB, 0x98, 0xD2, 0x77, 0xD9, 0xF5,
+      0xA5, 0x61, 0x1C, 0x2C, 0x9F, 0x41, 0x9D, 0x9F },
+    { 0x57, 0xED, 0xF4, 0xA2, 0x2B, 0xE3, 0xC9, 0x55,
+      0xAC, 0x49, 0xDA, 0x2E, 0x21, 0x07, 0xB6, 0x7A }
+};
+
+/*
+ * Checkup routine
+ */
+int md5_self_test( void )
+{
+    int i;
+    uchar md5sum[16];
+
+    for( i = 0; i < 7; i++ )
+    {
+        printf( "  MD5 test #%d: ", i + 1 );
+
+        md5_csum( (uchar *) md5_test_str[i],
+                    strlen( md5_test_str[i] ), md5sum );
+
+        if( memcmp( md5sum, md5_test_sum[i], 16 ) != 0 )
+        {
+            printf( "failed\n" );
+            return( 1 );
+        }
+
+        printf( "passed\n" );
+    }
+
+    printf( "\n" );
+    return( 0 );
+}
+#else
+int md5_self_test( void )
+{
+    printf( "MD5 self-test not available\n\n" );
+    return( 1 );
+}
+#endif
diff --git a/md5.h b/md5.h
new file mode 100644
index 0000000..cefa7c4
--- /dev/null
+++ b/md5.h
@@ -0,0 +1,57 @@
+#ifndef _MD5_H
+#define _MD5_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef _STD_TYPES
+#define _STD_TYPES
+
+#define uchar   unsigned char
+#define uint    unsigned int
+#define ulong   unsigned long int
+
+#endif
+
+typedef struct
+{
+    ulong total[2];
+    ulong state[4];
+    uchar buffer[64];
+}
+md5_context;
+
+/*
+ * Core MD5 functions
+ */
+void md5_starts( md5_context *ctx );
+void md5_update( md5_context *ctx, uchar *input, uint length );
+void md5_finish( md5_context *ctx, uchar digest[16] );
+
+/*
+ * Output MD5(file contents), returns 0 if successful.
+ */
+int md5_file( char *filename, uchar digest[16] );
+
+/*
+ * Output MD5(buf)
+ */
+void md5_csum( uchar *buf, uint buflen, uchar digest[16] );
+
+/*
+ * Output HMAC-MD5(key,buf)
+ */
+void md5_hmac( uchar *key, uint keylen, uchar *buf, uint buflen,
+               uchar digest[16] );
+
+/*
+ * Checkup routine
+ */
+int md5_self_test( void );
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* md5.h */
diff --git a/network.c b/network.c
new file mode 100644
index 0000000..8392ff6
--- /dev/null
+++ b/network.c
@@ -0,0 +1,305 @@
+/*
+ * most of this code is base on or copied from the
+ * "UNIX Network Programming" Book by W.Richard Stevens, Bill Fenner
+ * and Andrew M. Rudoff (ISBN: 0-13-141155-1)
+ */
+
+#include "debug.h"
+#include "network.h"
+
+/*
+ * tcp_connect()
+ *   connect to host:serv
+ */
+int
+tcp_connect(const char *host, const char *serv)
+{ /*{{{*/
+	int    sockfd, n;
+	struct addrinfo hints, *res, *ressave;
+
+	memset(&hints, 0, sizeof(struct addrinfo));
+	hints.ai_family = AF_UNSPEC;
+	hints.ai_socktype = SOCK_STREAM;
+
+	if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0) {
+		errno_msg("tcp_connect() error for %s, %s: %s",
+			  host, serv, gai_strerror(n));
+		return(-1);
+	}
+	ressave = res;
+
+	do {
+		sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
+		if (sockfd < 0)
+			continue; /* ignore this one */
+
+		if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)
+			break;    /* success */
+
+		xclose(sockfd);  /* ignore this one */
+	} while ( (res = res->ai_next) != NULL);
+
+	if (res == NULL)  /* errno set from final connect() */
+		errno_msg("tcp_connect() error for %s, %s", host, serv);
+
+	freeaddrinfo(ressave);
+
+	return(sockfd);
+} /*}}}*/
+
+/*
+ * sock_ntop()
+ *   protocol (IPv4 vs. IPv6) independent version of inet_ntop
+ */
+char *
+sock_ntop(const struct sockaddr *sa, socklen_t salen)
+{ /*{{{*/
+	char   portstr[8];
+	static char str[128];   /* Unix domain is largest */
+	struct sockaddr_in  *sin  = (struct sockaddr_in  *) sa;
+#ifdef AF_INET6
+	struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) sa;
+#endif
+
+	switch (sa->sa_family) {
+	case AF_INET:
+		if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)
+			return(NULL);
+		if (ntohs(sin->sin_port) != 0) {
+			snprintf(portstr, sizeof(portstr), ":%d", ntohs(sin->sin_port));
+			strcat(str, portstr);
+		}
+		return(str);
+
+#ifdef AF_INET6
+	case AF_INET6:
+		str[0] = '[';
+		if (inet_ntop(AF_INET6, &sin6->sin6_addr, str + 1, sizeof(str) - 1) == NULL)
+			return(NULL);
+		if (ntohs(sin6->sin6_port) != 0) {
+			snprintf(portstr, sizeof(portstr), "]:%d", ntohs(sin6->sin6_port));
+			strcat(str, portstr);
+			return(str);
+		}
+		return (str + 1);
+#endif
+
+	default:
+		snprintf(str, sizeof(str), "sock_ntop: unknown AF_xxx: %d, len %d",
+			 sa->sa_family, salen);
+		return(str);
+	}
+	return (NULL);
+} /*}}}*/
+
+/* wrapper for sock_ntop(), that handles error codes */
+char *
+xsock_ntop(const struct sockaddr *sa, socklen_t salen)
+{ /*{{{*/
+	char *ptr;
+
+	if ( (ptr = sock_ntop(sa, salen)) == NULL)
+		errno_msg("sock_ntop() error");
+	return(ptr);
+} /*}}}*/
+
+/*
+ * sock_ntop_host()
+ *   similar to sock_ntop(), but only returns the host portion (not the port)
+ */
+char *
+sock_ntop_host(const struct sockaddr *sa, socklen_t salen)
+{ /*{{{*/
+	static char str[128];   /* Unix domain is largest */
+	struct sockaddr_in  *sin = (struct sockaddr_in *) sa;
+#ifdef AF_INET6
+	struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) sa;
+#endif
+
+	switch (sa->sa_family) {
+	case AF_INET:
+		if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)
+			return(NULL);
+		return(str);
+
+#ifdef AF_INET6
+	case AF_INET6:
+		if (inet_ntop(AF_INET6, &sin6->sin6_addr, str, sizeof(str)) == NULL)
+			return(NULL);
+		return(str);
+#endif
+
+	default:
+		snprintf(str, sizeof(str), "sock_ntop_host: unknown AF_xxx: %d, len %d",
+		sa->sa_family, salen);
+		return(str);
+	}
+	return (NULL);
+} /*}}}*/
+
+/* wrapper for sock_ntop_host(), that handles error codes */
+char *
+xsock_ntop_host(const struct sockaddr *sa, socklen_t salen)
+{ /*{{{*/
+	char  *ptr;
+
+	if ( (ptr = sock_ntop_host(sa, salen)) == NULL)
+		errno_msg("sock_ntop_host() error");  /* inet_ntop() sets errno */
+	return(ptr);
+} /*}}}*/
+
+/*
+ * sock_get_port()
+ *   protocol independent way to get the port of a socket
+ */
+int
+sock_get_port(const struct sockaddr *sa, socklen_t salen)
+{ /*{{{*/
+	struct sockaddr_in  *sin  = (struct sockaddr_in  *) sa;
+#ifdef AF_INET6
+	struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) sa;
+#endif
+
+	switch (sa->sa_family) {
+	case AF_INET:
+		return(sin->sin_port);
+
+#ifdef AF_INET6
+	case AF_INET6:
+		return(sin6->sin6_port);
+#endif
+	}
+	return(-1);
+} /*}}}*/
+
+/*
+ * sock_set_addr()
+ *   protocol independent way to set the address of a socket
+ */
+void
+sock_set_addr(struct sockaddr *sa, socklen_t salen, const void *addr)
+{ /*{{{*/
+	struct sockaddr_in  *sin  = (struct sockaddr_in  *) sa;
+#ifdef AF_INET6
+	struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) sa;
+#endif
+
+	switch (sa->sa_family) {
+	case AF_INET:
+		memcpy(&sin->sin_addr, addr, sizeof(struct in_addr));
+	return;
+
+#ifdef AF_INET6
+	case AF_INET6:
+		memcpy(&sin6->sin6_addr, addr, sizeof(struct in6_addr));
+	return;
+#endif
+	}
+} /*}}}*/
+
+/*
+ * sock_set_addr()
+ *   protocol independent way to set the port of a socket
+ */
+void
+sock_set_port(struct sockaddr *sa, socklen_t salen, int port)
+{ /*{{{*/
+	struct sockaddr_in  *sin  = (struct sockaddr_in  *) sa;
+#ifdef AF_INET6
+	struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) sa;
+#endif
+
+	switch (sa->sa_family) {
+	case AF_INET:
+		sin->sin_port = port;
+		return;
+
+#ifdef AF_INET6
+	case AF_INET6:
+		sin6->sin6_port = port;
+		return;
+#endif
+	}
+	return;
+} /*}}}*/
+
+/*
+ * readn()
+ *   read 'n' bytes from a filedescriptor
+ */
+ssize_t
+readn(int fd, void *vptr, size_t n)
+{ /*{{{*/
+	size_t  nleft;
+	ssize_t nread;
+	char  *ptr;
+
+	ptr = vptr;
+	nleft = n;
+	while (nleft > 0) {
+		if ( (nread = read(fd, ptr, nleft)) < 0) {
+			if (errno == EINTR)
+				nread = 0;    /* and call read() again */
+			else
+				return(-1);
+		}
+		else if (nread == 0) {
+			break; /* EOF */
+		}
+
+		nleft -= nread;
+		ptr   += nread;
+	}
+	return(n - nleft);    /* return >= 0 */
+} /*}}}*/
+
+ssize_t
+xreadn(int fd, void *ptr, size_t nbytes)
+{ /*{{{*/
+	ssize_t   n;
+
+	if ( (n = readn(fd, ptr, nbytes)) < 0)
+		errno_msg("readn() error");
+	return(n);
+} /*}}}*/
+
+/*
+ * writen
+ *   write 'n' bytes to a descriptor
+ */
+ssize_t
+writen(int fd, const void *vptr, size_t n)
+{ /*{{{*/
+	size_t    nleft;
+	ssize_t   nwritten;
+	const char  *ptr;
+
+	ptr = vptr;
+	nleft = n;
+	while (nleft > 0) {
+		if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
+			if (nwritten < 0 && errno == EINTR)
+				nwritten = 0;   /* and call write() again */
+			else
+				return(-1);     /* error */
+		}
+
+		nleft -= nwritten;
+		ptr   += nwritten;
+	}
+	return(n);
+} /*}}}*/
+
+void
+xwriten(int fd, void *ptr, size_t nbytes)
+{ /*{{{*/
+	if (writen(fd, ptr, nbytes) != nbytes)
+		errno_msg("writen() error");
+} /*}}}*/
+
+void
+xclose(int fd)
+{ /*{{{*/
+	if (close(fd) == -1)
+		errno_msg("close() error");
+} /*}}}*/
diff --git a/network.h b/network.h
new file mode 100644
index 0000000..1b9cd65
--- /dev/null
+++ b/network.h
@@ -0,0 +1,37 @@
+#ifndef x__NETWORK_H
+#define x__NETWORK_H
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <arpa/inet.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdarg.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "compiler.h"
+
+#define MAX_ERR_LINE 512
+
+int      tcp_connect(const char *host, const char *serv);
+char    *sock_ntop(const struct sockaddr *sa, socklen_t salen);
+char    *sock_ntop_host(const struct sockaddr *sa, socklen_t salen);
+int      sock_get_port(const struct sockaddr *sa, socklen_t salen);
+void     sock_set_addr(struct sockaddr *sa, socklen_t salen, const void *ptr);
+void     sock_set_port(struct sockaddr *sa, socklen_t salen, int port);
+ssize_t  readn(int fd, void *vptr, size_t n);
+ssize_t  writen(int fd, const void *vptr, size_t n);
+
+/* wrapper functions */
+char    *xsock_ntop(const struct sockaddr *sa, socklen_t salen);
+char    *xsock_ntop_host(const struct sockaddr *sa, socklen_t salen);
+ssize_t  xreadn(int fd, void *ptr, size_t nbytes);
+void     xwriten(int fd, void *ptr, size_t nbytes);
+void     xclose(int fd);
+
+/* helper functions */
+void errno_msg(const char *fmt, ...) __FORMAT(1, 2);
+
+#endif /* x__NETWORK_H */
diff --git a/options.c b/options.c
index 8628e82..fc1990c 100644
--- a/options.c
+++ b/options.c
@@ -21,6 +21,7 @@
 #include "file.h"
 #include "prog.h"
 #include "output.h"
+#include "as.h"
 #include "config/datadir.h"
 
 #include <stdio.h>
@@ -186,6 +187,7 @@ static const char * const valid_sort_keys[] = {
 	"date",
 	"genre",
 	"filename",
+	"albumartist",
 	NULL
 };
 
@@ -255,6 +257,30 @@ static void set_lib_sort(unsigned int id, const char *buf)
 		editable_set_sort_keys(&lib_editable, keys);
 }
 
+static void get_as_pass(unsigned int id, char *buf)
+{
+	strncpy(buf, as_authinfo.pass, OPTION_MAX_SIZE-1);
+	buf[OPTION_MAX_SIZE-1]='\0';
+}
+
+static void set_as_pass(unsigned int id, const char *buf)
+{
+	strncpy(as_authinfo.pass, buf, OPTION_MAX_SIZE-1);
+	as_authinfo.pass[OPTION_MAX_SIZE-1]='\0';
+}
+
+static void get_as_user(unsigned int id, char *buf)
+{
+	strncpy(buf, as_authinfo.user, OPTION_MAX_SIZE-1);
+	buf[OPTION_MAX_SIZE-1]='\0';
+}
+
+static void set_as_user(unsigned int id, const char *buf)
+{
+	strncpy(as_authinfo.user, buf, OPTION_MAX_SIZE-1);
+	as_authinfo.pass[OPTION_MAX_SIZE-1]='\0';
+}
+
 static void get_pl_sort(unsigned int id, char *buf)
 {
 	strcpy(buf, pl_editable.sort_str);
@@ -377,6 +403,31 @@ static void toggle_auto_reshuffle(unsigned int id)
 	auto_reshuffle ^= 1;
 }
 
+static void get_as_enable(unsigned int id, char *buf)
+{
+	strcpy(buf, bool_names[as_enable]);
+}
+
+static void set_as_enable(unsigned int id, const char *buf)
+{
+	int old=as_enable;
+	if (parse_bool(buf, &as_enable) && !old) {
+		/* switch from off to on */
+		as_init();
+		return;
+	}
+	if (!as_enable && old) {
+		/* switch from on to off */
+		as_exit();
+	}
+}
+
+static void toggle_as_enable(unsigned int id)
+{
+	if (as_enable) set_as_enable(id, "false");
+	else           set_as_enable(id, "true");
+}
+
 static void get_continue(unsigned int id, char *buf)
 {
 	strcpy(buf, bool_names[player_cont]);
@@ -694,6 +745,9 @@ static const struct {
 	opt_toggle_cb toggle;
 } simple_options[] = {
 	DT(aaa_mode)
+	DT(as_enable)
+	DN(as_pass)
+	DN(as_user)
 	DT(auto_reshuffle)
 	DN(buffer_seconds)
 	DT(confirm_run)
@@ -756,9 +810,10 @@ static const struct {
 	{ "format_playlist",	" %-20a %02n. %t%= %y %d " },
 	{ "format_title",	"%a - %l - %t (%y)" },
 	{ "format_trackwin",	" %02n. %t%= %y %d " },
-
 	{ "lib_sort"	,	"artist album discnumber tracknumber title filename" },
 	{ "pl_sort",		"" },
+	{ "as_pass",		"" },
+	{ "as_user",		"" },
 	{ "id3_default_charset","ISO-8859-1" },
 	{ NULL, NULL }
 };
diff --git a/pl.c b/pl.c
index da460cf..dea4079 100644
--- a/pl.c
+++ b/pl.c
@@ -6,6 +6,7 @@
 #include "editable.h"
 #include "options.h"
 #include "xmalloc.h"
+#include "as.h"
 
 struct editable pl_editable;
 struct simple_track *pl_cur_track = NULL;
@@ -86,6 +87,7 @@ struct track_info *pl_set_selected(void)
 	if (list_empty(&pl_editable.head))
 		return NULL;
 
+	as_hook(AS_PL_ENTER);
 	window_get_sel(pl_editable.win, &sel);
 	return set_track(iter_to_simple_track(&sel));
 }
diff --git a/player.c b/player.c
index a68f89d..55fd732 100644
--- a/player.c
+++ b/player.c
@@ -26,6 +26,7 @@
 #include "xmalloc.h"
 #include "debug.h"
 #include "compiler.h"
+#include "as.h"
 
 #include <stdlib.h>
 #include <pthread.h>
@@ -207,6 +208,7 @@ static inline unsigned int buffer_second_size(void)
 
 static inline int get_next(char **filename)
 {
+	as_hook(AS_AUTONEXT);
 	return player_cbs->get_next(filename);
 }
 
diff --git a/spawn.c b/spawn.c
index 68b5ed3..77f519a 100644
--- a/spawn.c
+++ b/spawn.c
@@ -41,7 +41,7 @@ int spawn(char *argv[], int *status)
 		return -1;
 	} else if (pid == 0) {
 		/* child */
-		int dev_null, err;
+		int dev_null, err, i;
 
 		close(err_pipe[0]);
 		fcntl(err_pipe[1], F_SETFD, FD_CLOEXEC);
@@ -56,6 +56,10 @@ int spawn(char *argv[], int *status)
 		/* not interactive, close stdin */
 		close(0);
 
+		/* close unused fds */
+		for (i = 3; i < 30; i++)
+			close(i);
+
 		execvp(argv[0], argv);
 
 		/* error */
diff --git a/strlib.c b/strlib.c
new file mode 100644
index 0000000..cb183d1
--- /dev/null
+++ b/strlib.c
@@ -0,0 +1,357 @@
+/*
+ * strlib.[ch] - a string handling library
+ *  See strlib.h for notes about libowfat (the code this is based on).
+ *
+ * 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., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "debug.h"
+#include "strlib.h"
+
+/*
+ * {con,de}structor
+ */
+void str_init(string_t *st)
+{ /*{{{*/
+	st->s=0;
+	st->len=st->all=0;
+} /*}}}*/
+
+void str_free(string_t *st)
+{ /*{{{*/
+	if (st->s && st->all > 0) free(st->s);
+	st->s=0;
+	st->len=st->all=0;
+} /*}}}*/
+
+/*
+ * allocation functions
+ */
+int str_ready(string_t *st, unsigned long int len)
+{ /*{{{*/
+	/*
+	 * makes sure st can take up at least len bytes.
+	 * this code allocates a few more bytes tthan needed,
+	 * so that small additions can be made without realloc()s
+	 * return 1 on success 0 otherwise (as in: realloc() fails)
+	 */
+	int c=len+(len>>3)+30;
+	char *tmp;
+	if (!st->s || st->all<len) {
+		/* mem needed */
+		if (!(tmp = realloc(st->s, c))) return 0;
+		st->all=c;
+		st->s=tmp;
+	}
+	return 1;
+} /*}}}*/
+
+int str_readya(string_t *st, unsigned long int len)
+{ /*{{{*/
+	/*
+	 * adds at least len bytes to the current length of *st
+	 */
+	if (st->s) {
+		if (st->len + len < len) return 0;
+		return str_ready(st, st->len + len);
+	}
+	else
+		return str_ready(st, len);
+} /*}}}*/
+
+/*
+ * string copy functions
+ */
+int str_ncpy(string_t *st, const char *buf, unsigned long int len)
+{ /*{{{*/
+	/*
+	 * copy len bytes from *buf into *st
+	 * return 1 on success, 0 otherwise.
+	 */
+	unsigned long int i;
+	if (buf == NULL || len == 0) {
+		st->len=0;
+		if (st->all != 0)
+			st->s[0] = '\0';
+		return(1);
+	}
+	if (!str_ready(st, len)) return(0);
+	/*
+	 * FIXME: these copying loops should be put into a sep. function
+	 *        to keep the code maintainable.
+	 */
+	for (i=0; i <= len - 1; i++) {
+		st->s[i] = buf[i];
+	}
+	st->s[len] = '\0';
+	st->len=len;
+	return(1);
+} /*}}}*/
+
+int str_cpy(string_t *st0, string_t *st1)
+{ /*{{{*/
+	/*
+	 * copy *st1's string into *st0
+	 */
+	return(str_ncpy(st0, st1->s, st1->len));
+} /*}}}*/
+
+/*
+ * concatenation functions
+ */
+int str_ncat(string_t *st, const char *buf, unsigned long int len)
+{ /*{{{*/
+	/*
+	 * add *buf to *st
+	 */
+	unsigned long int i;
+	if (len == 0 || buf == NULL) {
+		return(1);
+	}
+	if (!str_readya(st, len+1)) return(0);
+	for (i=st->len; i <= st->len + len - 1; i++) {
+		st->s[i] = buf[i - st->len];
+	}
+	st->len+=len;
+	st->s[st->len]='\0';
+	return(1);
+} /*}}}*/
+
+int str_cat(string_t *st0, string_t *st1)
+{ /*{{{*/
+	/*
+	 * add *st1 to *st0 (*st1 must be allocated already);
+	 */
+	return(str_ncat(st0, st1->s, st1->len));
+} /*}}}*/
+
+/*
+ * comparison functions
+ */
+int str_compstart(string_t *st, const char *buf)
+{ /*{{{*/
+	/* 
+	 * check if *st starts with *buf.
+	 * return 1 if that's the case, 0 otherwise
+	 */
+	unsigned long int i;
+	unsigned long int len=strlen(buf) - 1;
+	if (st->len <= len) return(0);
+	for (i=0; i<=len; i++)
+		if (st->s[i]!=buf[i]) return(0);
+	return(1);
+} /*}}}*/
+
+int str_scomp(string_t *st, const char *buf)
+{ /*{{{*/
+	/*
+	 * lexically compare *st with the *buf character array.
+	 * return -1, 1, or 0
+	 */
+	unsigned long int i, j;
+	for (i=0;;++i) {
+		if (i==st->len)
+			return( !buf[i] ? 0 : -1 );
+		if (!buf[i])
+			return 1;
+		j = (unsigned char)(st->s[i]) - (unsigned char)(buf[i]);
+		if (j)
+			return( j>0 ? 1 : -1 );
+	}
+	return(0);                                                                                                                                
+} /*}}}*/
+
+int str_comp(string_t *st0, string_t *st1)
+{ /*{{{*/
+	/*
+	 * lexically compare *st0 with *st1
+	 * return -1, 1 or 0
+	 */
+	return(str_scomp(st0, st1->s));
+} /*}}}*/
+
+/*
+ * replacement functions
+ */
+int str_nreplace(string_t *st, unsigned long int idx, unsigned long int offset, const char *replacement, unsigned long int rlen)
+{ /*{{{*/
+	/*
+	 * replace offset bytes from idx in *st with len bytes from
+	 * replacement; return 1 on success, 0 on memory problems, negative
+	 * values upon logical errors in parameters.
+	 */
+	char *rest;
+	int restlen;
+
+	if (idx > st->len)
+		return(-2);
+	if (idx+offset > st->len)
+		return(-3);
+
+	restlen = st->len - idx - offset;
+	if (!(rest = malloc(restlen * sizeof(char) + 1)))
+		return(0);
+
+	strncpy(rest, st->s+idx+offset, restlen); /* st->s+st->len is '\0' */
+	rest[restlen] = '\0';
+	if (rlen > offset) {
+		/* we might need to allocate more memory */
+		if (!str_readya(st, rlen-offset)) {
+			free(rest);
+			return(0);
+		}
+	}
+	st->s[idx] = '\0';
+	st->len = idx;
+	str_ncat(st, replacement, rlen);
+	str_ncat(st, rest, restlen);
+	free(rest);
+	return(1);
+} /*}}}*/
+
+int str_replace(string_t *st, unsigned long int idx, unsigned long int offset, string_t *replacement)
+{ /*{{{*/
+	return(str_nreplace(st, idx, offset, replacement->s, replacement->len));
+} /*}}}*/
+
+/*
+ * search functions
+ */
+int
+str_ngetidx(string_t *haystack, unsigned long int offset, const char *needle, unsigned long int nlen, unsigned long int *idx)
+{ /*{{{*/
+	unsigned long int i, j;
+	if (nlen == 0 || haystack->len == 0 || haystack->len < offset || nlen > haystack->len-offset)
+		return(0);
+
+	for (i=0; i<= haystack->len-offset-1; ++i) {
+		if (haystack->s[offset+i] == needle[0]) {
+			/* potential hit */
+			if (nlen == 1) {
+				if (idx != NULL)
+					*idx=i+offset;
+				return(1);
+			}
+			for (j=1; j<=nlen-1; ++j) {
+				if (haystack->s[offset+i+j] != needle[j])
+					break;
+				if (haystack->s[offset+i+j] == needle[j] && j == nlen-1) {
+					if (idx != NULL)
+						*idx=i+offset;
+					return(1);
+				}
+			}
+		}
+	}
+	return (0);
+} /*}}}*/
+
+int
+str_getidx(string_t *haystack, unsigned long int offset, string_t *needle, unsigned long int *idx)
+{ /*{{{*/
+	return(str_ngetidx(haystack, offset, needle->s, needle->len, idx));
+} /*}}}*/
+
+/*
+ * conversion functions
+ */
+int
+str_nlat1_to_utf8(string_t *converted, const unsigned char *input, unsigned long len)
+{ /*{{{*/
+	unsigned long int nc=0, olen=0, ilen;
+	unsigned char *in;
+
+	in = (unsigned char *)input;
+	ilen = 0;
+	while (ilen++ <= len)
+		if (*(in++) > 0x7f)
+			++nc;
+
+	if (nc) {
+		if (!str_readya(converted, nc))
+			return(0);
+	}
+	else
+		return(1);
+
+	in = (unsigned char *)input;
+	ilen = 0;
+	while (ilen++ <= len) {
+		if (*in > 0x7f) {
+			converted->s[olen++] = 0xC0 | (*in >> 6);
+			*in = (0x80 | (*in & 0x3F));
+		}
+		converted->s[olen++] = *in;
+		in++;
+	}
+	converted->s[olen] = '\0';
+	converted->len = olen;
+	return(1);
+} /*}}}*/
+
+int
+str_lat1_to_utf8(string_t *converted, string_t *input)
+{ /*{{{*/
+	return(str_nlat1_to_utf8(converted, input->s, input->len));
+} /*}}}*/
+
+/*
+ * Perl lookalikes
+ */
+int str_chop(string_t *st)
+{ /*{{{*/
+	/*
+	 * remove last character of *st
+	 * return removed byte or -1 if *st if empty
+	 */
+	int r;
+	if (!st->len)
+		return(-1);
+	r=st->s[st->len-1];
+	st->s[st->len--]=0;
+	return(r);
+} /*}}}*/                                                                                                                                           
+                                                                                                                                            
+int str_chomp(string_t *st)
+{ /*{{{*/
+	/*
+	 * remove "\r" "\n" "\r\n" from *st's end.
+	 * return count of rem. chars (poss. values: 0,1,2);
+	 */
+	if (!st->len)
+		return(0);
+	if (st->len==1) {
+		if (st->s[st->len-1] == '\r' || st->s[st->len-1] == '\n') {
+			st->s[0]=0; st->len=0;
+			return(1);
+		}
+		else
+			return(0);
+	}
+	if (st->s[st->len-1] == '\r' || st->s[st->len-1] == '\n') {
+		if (st->s[st->len-1] == '\n' && st->s[st->len-2]) {
+			st->s[st->len-2]=0;
+			st->len-=2;
+			return(2);
+		}
+		st->s[--st->len]=0;
+		return(1);
+	}
+	return(0);
+} /*}}}*/
diff --git a/strlib.h b/strlib.h
new file mode 100644
index 0000000..31c0194
--- /dev/null
+++ b/strlib.h
@@ -0,0 +1,88 @@
+/*
+ * strlib.[ch] - a string handling library
+ *  Some time ago, I've been writing a few functions to handle strings,
+ *  mainly as an exercise. When someone hinted me at fefe's libowfat[1],
+ *  I found that the basic idea was quite similar, but libowfat was
+ *  doing a lot of things quite smart, so I rewrote my functions while
+ *  looking at the libowfat sources. So, this code is heavily based
+ *  on these sources.
+ *
+ * Note, that libowfat is not only about strings, it does lots of
+ * things, you might find useful. Take a look at it!
+ *
+ * [1] http://www.fefe.de/libowfat/
+ *
+ * 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., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ */
+
+#ifndef x__STRLIB_H
+#define x__STRLIB_H
+
+/* internal structure */
+typedef struct {
+	/*
+	 *  s:    string
+	 *  len:  length of string
+	 *  all:  allocated bytes for *s
+	 */
+	char *s;
+	unsigned long int len;
+	unsigned long int all;
+} string_t;
+
+/* {con,de}structor */
+void  str_init(string_t *st);
+void  str_free(string_t *st);
+
+/* allocation functions */
+int   str_ready(string_t *st, unsigned long int len);
+int   str_readya(string_t *st, unsigned long int len);
+
+/* string copy functions */
+int   str_ncpy(string_t *st, const char *buf, unsigned long int len);
+#define str_scpy(st, buf) str_ncpy(st, buf, ( buf == NULL ? 0 : strlen(buf) ))
+int   str_cpy(string_t *st0, string_t *st1);
+
+/* concatenation functions */
+int   str_ncat(string_t *st, const char *buf, unsigned long int len);
+#define str_scat(st, buf) str_ncat(st, buf, ( buf == NULL ? 0 : strlen(buf) ))
+int   str_cat(string_t *st0, string_t *st1);
+
+/* comparison functions */
+int   str_compstart(string_t *st, const char *buf);
+int   str_scomp(string_t *st, const char *buf);
+int   str_comp(string_t *st0, string_t *st1);
+
+/* replacement functions */
+int str_nreplace(string_t *st, unsigned long int idx, unsigned long int offset, const char *replacement, unsigned long int rlen);
+#define str_sreplace(st, idx, offset, repl) str_nreplace(st, idx, offset, repl, strlen(repl))
+int str_replace(string_t *st, unsigned long int idx, unsigned long int offset, string_t *replacement);
+
+/* search functions */
+int str_ngetidx(string_t *haystack, unsigned long int offset, const char *needle, unsigned long int nlen, unsigned long int *idx);
+#define str_sgetidx(hs, offset, n, idx) str_ngetidx(hs, offset, n, strlen(n), idx)
+int str_getidx(string_t *haystack, unsigned long int offset, string_t *needle, unsigned long int *idx);
+
+/* conversion functions */
+int str_nlat1_to_utf8(string_t *converted, const unsigned char *input, unsigned long len);
+#define str_slat1_to_utf8(c, s) str_nlat1_to_utf8(c, s, strlen(s))
+int str_lat1_to_utf8(string_t *converted, string_t *input);
+
+/* Perl lookalikes */
+int   str_chop(string_t *st);
+int   str_chomp(string_t *st);
+
+#endif /* x__STRLIB_H */
diff --git a/tabexp.c b/tabexp.c
index c7a5a18..271da5f 100644
--- a/tabexp.c
+++ b/tabexp.c
@@ -1,22 +1,3 @@
-/* 
- * Copyright 2004-2005 Timo Hirvonen
- * 
- * 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., 59 Temple Place - Suite 330, Boston, MA
- * 02111-1307, USA.
- */
-
 #include "tabexp.h"
 #include "xmalloc.h"
 #include "xstrjoin.h"
@@ -26,51 +7,36 @@
 
 struct tabexp tabexp = {
 	.head = NULL,
-	.tails = NULL,
-	.nr_tails = 0,
+	.tails = NULL
 };
 
-/* index to tabexp.tails */
-static int tabexp_index = 0;
-
 char *tabexp_expand(const char *src, void (*load_matches)(const char *src))
 {
-	if (tabexp.tails == NULL) {
-		char *expanded;
+	static int idx = 0;
+	char *expanded;
 
+	if (tabexp.tails == NULL) {
 		load_matches(src);
-
 		if (tabexp.tails == NULL) {
 			BUG_ON(tabexp.head != NULL);
-			BUG_ON(tabexp.nr_tails != 0);
 			return NULL;
 		}
-
 		BUG_ON(tabexp.head == NULL);
-		BUG_ON(tabexp.nr_tails < 1);
-
-		expanded = xstrjoin(tabexp.head, tabexp.tails[0]);
-		if (tabexp.nr_tails == 1)
-			tabexp_reset();
-		return expanded;
-	} else {
-		tabexp_index++;
-		tabexp_index %= tabexp.nr_tails;
-		return xstrjoin(tabexp.head, tabexp.tails[tabexp_index]);
+		idx = 0;
 	}
+
+	expanded = xstrjoin(tabexp.head, tabexp.tails[idx++]);
+	if (!tabexp.tails[idx])
+		idx = 0;
+	if (!tabexp.tails[1])
+		tabexp_reset();
+	return expanded;
 }
 
 void tabexp_reset(void)
 {
-	int i;
-
-	for (i = 0; i < tabexp.nr_tails; i++)
-		free(tabexp.tails[i]);
-	free(tabexp.tails);
+	free_str_array(tabexp.tails);
 	free(tabexp.head);
 	tabexp.tails = NULL;
 	tabexp.head = NULL;
-	tabexp.nr_tails = 0;
-
-	tabexp_index = 0;
 }
diff --git a/tabexp.h b/tabexp.h
index 022e87c..d28d17c 100644
--- a/tabexp.h
+++ b/tabexp.h
@@ -23,7 +23,6 @@
 struct tabexp {
 	char *head;
 	char **tails;
-	int nr_tails;
 };
 
 extern struct tabexp tabexp;
diff --git a/tabexp_file.c b/tabexp_file.c
index 4dd08df..db64fd8 100644
--- a/tabexp_file.c
+++ b/tabexp_file.c
@@ -128,7 +128,6 @@ static void tabexp_load_dir(const char *dirname, const char *start,
 
 		tabexp.head = xstrdup(dirname);
 		tabexp.tails = array.ptrs;
-		tabexp.nr_tails = array.count;
 	}
 out:
 	free(full_dir_name);
@@ -157,7 +156,6 @@ void expand_files_and_dirs(const char *src,
 
 			if (home) {
 				tabexp.head = xstrdup("");
-				tabexp.nr_tails = 1;
 				tabexp.tails = xnew(char *, 2);
 				tabexp.tails[0] = home;
 				tabexp.tails[1] = NULL;
diff --git a/track.c b/track.c
index c8c9ef8..92271bf 100644
--- a/track.c
+++ b/track.c
@@ -14,24 +14,10 @@
 
 #include <string.h>
 
-static int xstrcasecmp(const char *a, const char *b)
-{
-	if (a == NULL) {
-		if (b == NULL)
-			return 0;
-		return -1;
-	} else if (b == NULL) {
-		return 1;
-	}
-	return u_strcasecmp(a, b);
-}
-
 void simple_track_init(struct simple_track *track, struct track_info *ti)
 {
 	track->info = ti;
 	track->marked = 0;
-	track->disc = comments_get_int(ti->comments, "discnumber");
-	track->num = comments_get_int(ti->comments, "tracknumber");
 }
 
 struct simple_track *simple_track_new(struct track_info *ti)
@@ -164,48 +150,6 @@ again:
 	return NULL;
 }
 
-int simple_track_cmp(const struct list_head *a_head, const struct list_head *b_head, const char * const *keys)
-{
-	const struct simple_track *a = to_simple_track(a_head);
-	const struct simple_track *b = to_simple_track(b_head);
-	int i, res = 0;
-
-	for (i = 0; keys[i]; i++) {
-		const char *key = keys[i];
-		const char *av, *bv;
-		const struct track_info *ai, *bi;
-
-		/* numeric compare for tracknumber and discnumber */
-		if (strcmp(key, "tracknumber") == 0) {
-			res = a->num - b->num;
-			if (res)
-				break;
-			continue;
-		}
-		if (strcmp(key, "discnumber") == 0) {
-			res = a->disc - b->disc;
-			if (res)
-				break;
-			continue;
-		}
-		ai = a->info;
-		bi = b->info;
-		if (strcmp(key, "filename") == 0) {
-			/* NOTE: filenames are not necessarily UTF-8 */
-			res = strcasecmp(ai->filename, bi->filename);
-			if (res)
-				break;
-			continue;
-		}
-		av = comments_get_val(ai->comments, key);
-		bv = comments_get_val(bi->comments, key);
-		res = xstrcasecmp(av, bv);
-		if (res)
-			break;
-	}
-	return res;
-}
-
 void sorted_list_add_track(struct list_head *head, struct simple_track *track, const char * const *keys)
 {
 	struct list_head *item;
@@ -215,7 +159,10 @@ void sorted_list_add_track(struct list_head *head, struct simple_track *track, c
 	 */
 	item = head->prev;
 	while (item != head) {
-		if (simple_track_cmp(&track->node, item, keys) >= 0)
+		const struct simple_track *a = to_simple_track(&track->node);
+		const struct simple_track *b = to_simple_track(item);
+
+		if (track_info_cmp(a->info, b->info, keys) >= 0)
 			break;
 		item = item->prev;
 	}
diff --git a/track.h b/track.h
index 271b5ae..df25e37 100644
--- a/track.h
+++ b/track.h
@@ -11,14 +11,7 @@
 struct simple_track {
 	struct list_head node;
 	struct track_info *info;
-
 	unsigned int marked : 1;
-
-	/* -1.. */
-	int disc : 15;
-
-	/* -1.. */
-	int num : 16;
 };
 
 struct shuffle_track {
@@ -71,7 +64,6 @@ struct simple_track *simple_list_get_next(struct list_head *head, struct simple_
 struct simple_track *simple_list_get_prev(struct list_head *head, struct simple_track *cur,
 		int (*filter)(const struct simple_track *));
 
-int simple_track_cmp(const struct list_head *a_head, const struct list_head *b_head, const char * const *keys);
 void sorted_list_add_track(struct list_head *head, struct simple_track *track, const char * const *keys);
 
 void shuffle_list_add_track(struct list_head *head, struct list_head *node, int nr);
diff --git a/track_info.c b/track_info.c
index 04f48f6..d9fa28d 100644
--- a/track_info.c
+++ b/track_info.c
@@ -105,3 +105,54 @@ int track_info_matches(struct track_info *ti, const char *text, unsigned int fla
 	free_str_array(words);
 	return matched;
 }
+
+static int xstrcasecmp(const char *a, const char *b)
+{
+	if (a == NULL) {
+		if (b == NULL)
+			return 0;
+		return -1;
+	} else if (b == NULL) {
+		return 1;
+	}
+	return u_strcasecmp(a, b);
+}
+
+int track_info_cmp(const struct track_info *a, const struct track_info *b, const char * const *keys)
+{
+	int i, res = 0;
+
+	for (i = 0; keys[i]; i++) {
+		const char *key = keys[i];
+		const char *av, *bv;
+
+		/* numeric compare for tracknumber and discnumber */
+		if (strcmp(key, "tracknumber") == 0) {
+			res = comments_get_int(a->comments, key) -
+				comments_get_int(b->comments, key);
+			if (res)
+				break;
+			continue;
+		}
+		if (strcmp(key, "discnumber") == 0) {
+			res = comments_get_int(a->comments, key) -
+				comments_get_int(b->comments, key);
+			if (res)
+				break;
+			continue;
+		}
+		if (strcmp(key, "filename") == 0) {
+			/* NOTE: filenames are not necessarily UTF-8 */
+			res = strcasecmp(a->filename, b->filename);
+			if (res)
+				break;
+			continue;
+		}
+		av = comments_get_val(a->comments, key);
+		bv = comments_get_val(b->comments, key);
+		res = xstrcasecmp(av, bv);
+		if (res)
+			break;
+	}
+	return res;
+}
diff --git a/track_info.h b/track_info.h
index 9149dac..16ba284 100644
--- a/track_info.h
+++ b/track_info.h
@@ -54,4 +54,6 @@ extern int track_info_has_tag(const struct track_info *ti);
  */
 extern int track_info_matches(struct track_info *ti, const char *text, unsigned int flags);
 
+int track_info_cmp(const struct track_info *a, const struct track_info *b, const char * const *keys);
+
 #endif
diff --git a/tree.c b/tree.c
index 5c369cf..054bf53 100644
--- a/tree.c
+++ b/tree.c
@@ -8,6 +8,7 @@
 #include "comment.h"
 #include "utils.h"
 #include "debug.h"
+#include "as.h"
 
 struct searchable *tree_searchable;
 struct window *lib_tree_win;
@@ -365,6 +366,7 @@ struct track_info *tree_set_selected(void)
 	if (list_empty(&lib_artist_head))
 		return NULL;
 
+	as_hook(AS_TREE_ENTER);
 	tree_win_get_selected(&artist, &album);
 	if (album == NULL) {
 		/* only artist selected, track window is empty
@@ -485,7 +487,7 @@ static void album_add_track(struct album *album, struct tree_track *track)
 		const struct simple_track *a = (const struct simple_track *)track;
 		const struct simple_track *b = (const struct simple_track *)to_tree_track(item);
 
-		if (simple_track_cmp(&a->node, &b->node, album_track_sort_keys) < 0)
+		if (track_info_cmp(a->info, b->info, album_track_sort_keys) < 0)
 			break;
 	}
 	/* add before item */
@@ -514,8 +516,7 @@ void tree_add_track(struct tree_track *track)
 		album_name = "<No Name>";
 
 	if (albumartist) {
-		/* FIXME: user might want to create custom categories  */
-		artist_name = "<Compilations>";
+		artist_name = albumartist;
 	} else {
 		const char *compilation = comments_get_val(ti->comments, "compilation");
 
@@ -536,7 +537,7 @@ void tree_add_track(struct tree_track *track)
 			window_changed(lib_track_win);
 		}
 	} else if (artist) {
-		date = comments_get_int(ti->comments, "date");
+		date = comments_get_date(ti->comments, "date");
 		album = artist_add_album(artist, album_name, date);
 		album_add_track(album, track);
 
@@ -546,7 +547,7 @@ void tree_add_track(struct tree_track *track)
 			/* album is not selected => no need to update track_win */
 		}
 	} else {
-		date = comments_get_int(ti->comments, "date");
+		date = comments_get_date(ti->comments, "date");
 		artist = add_artist(artist_name);
 		album = artist_add_album(artist, album_name, date);
 		album_add_track(album, track);
diff --git a/ui_curses.c b/ui_curses.c
index 8d4febe..87eac1d 100644
--- a/ui_curses.c
+++ b/ui_curses.c
@@ -56,7 +56,7 @@
 #include <signal.h>
 #include <stdarg.h>
 
-#if defined(__sun__)
+#if defined(__sun__) || defined(__CYGWIN__)
 /* TIOCGWINSZ */
 #include <termios.h>
 #include <ncurses.h>
@@ -470,44 +470,6 @@ static inline void fopt_set_time(struct format_option *fopt, int value, int empt
 	fopt->empty = empty;
 }
 
-static void fill_track_fopts(struct tree_track *track)
-{
-	const char *filename;
-	const struct track_info *ti = tree_track_info(track);
-	int num, disc;
-
-	if (using_utf8) {
-		filename = ti->filename;
-	} else {
-		utf8_encode(ti->filename);
-		filename = conv_buffer;
-	}
-	disc = track->shuffle_track.simple_track.disc;
-	num = track->shuffle_track.simple_track.num;
-
-	fopt_set_str(&track_fopts[TF_ARTIST], track->album->artist->name);
-	fopt_set_str(&track_fopts[TF_ALBUM], track->album->name);
-	fopt_set_int(&track_fopts[TF_DISC], disc, disc == -1);
-	fopt_set_int(&track_fopts[TF_TRACK], num, num == -1);
-	fopt_set_str(&track_fopts[TF_TITLE], comments_get_val(ti->comments, "title"));
-	fopt_set_str(&track_fopts[TF_YEAR], comments_get_val(ti->comments, "date"));
-	fopt_set_str(&track_fopts[TF_GENRE], comments_get_val(ti->comments, "genre"));
-	fopt_set_time(&track_fopts[TF_DURATION], ti->duration, ti->duration == -1);
-	fopt_set_str(&track_fopts[TF_PATHFILE], filename);
-	if (is_url(ti->filename)) {
-		fopt_set_str(&track_fopts[TF_FILE], filename);
-	} else {
-		const char *f;
-
-		f = strrchr(filename, '/');
-		if (f) {
-			fopt_set_str(&track_fopts[TF_FILE], f + 1);
-		} else {
-			fopt_set_str(&track_fopts[TF_FILE], filename);
-		}
-	}
-}
-
 static void fill_track_fopts_track_info(struct track_info *info)
 {
 	char *filename;
@@ -563,7 +525,7 @@ static void print_track(struct window *win, int row, struct iter *iter)
 		cursor_y = 1 + row;
 	}
 
-	fill_track_fopts(track);
+	fill_track_fopts_track_info(tree_track_info(track));
 
 	if (track_info_has_tag(tree_track_info(track))) {
 		format_print(print_buffer, track_win_w, track_win_format, track_fopts);
@@ -1350,6 +1312,32 @@ void error_msg(const char *format, ...)
 	}
 }
 
+/*
+ * errno_msg()
+ *   print an error message plus the current errno message
+ */
+void
+errno_msg(const char *format, ...)
+{
+	/*
+	 * This function is a little limited, as is outputs lines of
+	 * MAX_ERR_LINE chars at most. It's useful, though, and could
+	 * probably be used more widespread.
+	 */
+	char buf[MAX_ERR_LINE];
+	va_list ap;
+	int  errno_save = errno;
+	int  n;
+
+	va_start(ap, format);
+	vsnprintf(buf, MAX_ERR_LINE, format, ap);
+	va_end(ap);
+	n = strlen(buf);
+	snprintf(buf + n, MAX_ERR_LINE - n, ": %s", strerror(errno_save));
+	strcat(buf, "\n");
+	error_msg(buf);
+}
+
 int yes_no_query(const char *format, ...)
 {
 	char buffer[512];
@@ -1981,7 +1969,7 @@ static void init_curses(void)
 		 * xterm:            xterm, xterm-{,16,88,256}color
 		 */
 		if (!strcmp(term, "screen")) {
-			t_ts = "\033k";
+			t_ts = "\033_";
 			t_fs = "\033\\";
 		} else if (!strncmp(term, "xterm", 5) ||
 			   !strncmp(term, "rxvt", 4) ||
diff --git a/ui_curses.h b/ui_curses.h
index 55e07e6..7f43960 100644
--- a/ui_curses.h
+++ b/ui_curses.h
@@ -23,6 +23,8 @@
 #include "search.h"
 #include "compiler.h"
 
+#define MAX_ERR_LINE 512
+
 enum ui_input_mode {
 	NORMAL_MODE,
 	COMMAND_MODE,
@@ -44,6 +46,7 @@ void update_colors(void);
 void update_full(void);
 void info_msg(const char *format, ...) __FORMAT(1, 2);
 void error_msg(const char *format, ...) __FORMAT(1, 2);
+void errno_msg(const char *format, ...) __FORMAT(1, 2);
 int yes_no_query(const char *format, ...) __FORMAT(1, 2);
 void search_not_found(void);
 void set_view(int view);
diff --git a/vorbis.c b/vorbis.c
index c60b995..da0ec8e 100644
--- a/vorbis.c
+++ b/vorbis.c
@@ -252,6 +252,10 @@ static int vorbis_read_comments(struct input_plugin_data *ip_data,
 			continue;
 		}
 		key = xstrndup(str, i);
+		if (!strcasecmp(key, "album_artist")) {
+			free(key);
+			key = xstrdup("albumartist");
+		}
 		if (!is_interesting_key(key)) {
 			free(key);
 			continue;
diff --git a/waveout.c b/waveout.c
new file mode 100644
index 0000000..e1b86bb
--- /dev/null
+++ b/waveout.c
@@ -0,0 +1,312 @@
+/* 
+ * Copyright 2007 dnk <dnk@bjum.net>
+ * Based on oss.c
+ * 
+ * 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., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ */
+
+#include "op.h"
+#include "sf.h"
+#include "utils.h"
+#include "xmalloc.h"
+#include "debug.h"
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <mmsystem.h>
+
+static HWAVEOUT wave_out;
+static sample_format_t waveout_sf;
+static int buffer_size = 4096;
+static int buffer_count = 12;
+static WAVEHDR *buffers;
+static int buffer_idx;
+static int buffer_space;
+
+
+static void waveout_error(const char *name, int rc)
+{
+	const char *errstr = "UNKNOWN";
+
+	switch (rc) {
+	case MMSYSERR_ALLOCATED:	errstr = "MMSYSERR_ALLOCATED"; break;
+	case MMSYSERR_INVALHANDLE:	errstr = "MMSYSERR_INVALHANDLE"; break;
+	case MMSYSERR_NODRIVER:		errstr = "MMSYSERR_NODRIVER"; break;
+	case MMSYSERR_BADDEVICEID:	errstr = "MMSYSERR_BADDEVICEID"; break;
+	case MMSYSERR_NOMEM:		errstr = "MMSYSERR_NOMEM"; break;
+	case MMSYSERR_NOTSUPPORTED:	errstr = "MMSYSERR_NOTSUPPORTED"; break;
+	case WAVERR_STILLPLAYING:	errstr = "WAVERR_STILLPLAYING"; break;
+	case WAVERR_UNPREPARED:		errstr = "WAVERR_UNPREPARED"; break;
+	case WAVERR_BADFORMAT:		errstr = "WAVERR_BADFORMAT"; break;
+	case WAVERR_SYNC:		errstr = "WAVERR_SYNC"; break;
+	}
+
+	d_print("%s returned error %s (%d)\n", name, errstr, rc);
+}
+
+static void clean_buffers(void)
+{
+	int i;
+
+	/* mark used buffers clean */
+	for (i = 0; i < buffer_count; i++) {
+		WAVEHDR *hdr = &buffers[(buffer_idx + i) % buffer_count];
+
+		if (!(hdr->dwFlags & WHDR_DONE))
+			break;
+		buffer_space += hdr->dwBufferLength;
+		waveOutUnprepareHeader(wave_out, hdr, sizeof(WAVEHDR));
+		hdr->dwFlags = 0;
+	}
+}
+
+static int waveout_open(sample_format_t sf)
+{
+	WAVEFORMATEX  format;
+	int rc, i;
+
+	/* WAVEFORMATEX does not support channels > 2, waveOutWrite() wants little endian signed PCM */
+	if (sf_get_bigendian(sf) || !sf_get_signed(sf) || sf_get_channels(sf) > 2) {
+		return -OP_ERROR_SAMPLE_FORMAT;
+	}
+
+	memset(&format, 0, sizeof(format));
+	format.cbSize = sizeof(format);
+	format.wFormatTag = WAVE_FORMAT_PCM;
+	format.nChannels = sf_get_channels(sf);
+	format.nSamplesPerSec = sf_get_rate(sf);
+	format.wBitsPerSample = sf_get_bits(sf);
+	format.nAvgBytesPerSec = sf_get_second_size(sf);
+	format.nBlockAlign = sf_get_frame_size(sf);
+
+	if ((rc = waveOutOpen(&wave_out, WAVE_MAPPER, &format, 0, 0, CALLBACK_NULL)) != MMSYSERR_NOERROR) {
+		switch (rc) {
+		case MMSYSERR_ALLOCATED:
+			errno = EBUSY;
+			return -OP_ERROR_ERRNO;
+		case MMSYSERR_BADDEVICEID:
+		case MMSYSERR_NODRIVER:
+			errno = ENODEV;
+			return -OP_ERROR_ERRNO;
+		case MMSYSERR_NOMEM:
+			errno = ENOMEM;
+			return -OP_ERROR_ERRNO;
+		case WAVERR_BADFORMAT:
+			return -OP_ERROR_SAMPLE_FORMAT;
+		}
+		return -OP_ERROR_INTERNAL;
+	}
+
+	/* reset buffers */
+	for (i = 0; i < buffer_count; i++) {
+		buffers[i].dwFlags = 0;
+	}
+	buffer_idx = 0;
+	buffer_space = buffer_size * buffer_count;
+
+	waveout_sf = sf;
+
+	return 0;
+}
+
+static int waveout_close(void)
+{
+	int rc;
+
+	waveOutReset(wave_out);
+
+	clean_buffers();
+
+	if ((rc = waveOutClose(wave_out)) != MMSYSERR_NOERROR) {
+		waveout_error("waveOutClose", rc);
+		return -1;
+	}
+	wave_out = NULL;
+
+	return 0;
+}
+
+static int waveout_init(void)
+{
+	WAVEHDR *hdr;
+	int i;
+
+	/* create buffers */
+	buffers = xnew(WAVEHDR, buffer_count);
+	for (i = 0; i < buffer_count; i++) {
+		hdr = &buffers[i];
+
+		memset(hdr, 0, sizeof(WAVEHDR));
+		hdr->lpData = xmalloc(buffer_size);
+	}
+	return 0;
+}
+
+static int waveout_exit(void)
+{
+	int i;
+
+	for (i = 0; i < buffer_count; i++) {
+		free(buffers[i].lpData);
+	}
+	free(buffers);
+	buffers = NULL;
+
+	return 0;
+}
+
+static int waveout_write(const char *buffer, int count)
+{
+	int written = 0;
+	int len, rc;
+
+	count /= sf_get_frame_size(waveout_sf);
+	count *= sf_get_frame_size(waveout_sf);
+
+	clean_buffers();
+
+	while (count > 0) {
+		WAVEHDR *hdr = &buffers[buffer_idx];
+
+		if (hdr->dwFlags != 0) {
+			/* no free buffers */
+			break;
+		}
+
+		len = min(count, buffer_size);
+		hdr->dwBufferLength = len;
+		memcpy(hdr->lpData, buffer + written, len);
+
+		if ((rc = waveOutPrepareHeader(wave_out, hdr, sizeof(WAVEHDR))) != MMSYSERR_NOERROR) {
+			waveout_error("waveOutPrepareHeader", rc);
+			break;
+		}
+
+		if ((rc = waveOutWrite(wave_out, hdr, sizeof(WAVEHDR))) != MMSYSERR_NOERROR) {
+			waveOutUnprepareHeader(wave_out, hdr, sizeof(WAVEHDR));
+			hdr->dwFlags = 0;
+			waveout_error("waveOutWrite", rc);
+			break;
+		}
+ 
+		written += len;
+		count -= len;
+		buffer_idx = (buffer_idx + 1) % buffer_count;
+	}
+
+	buffer_space -= written;
+
+	return written;
+}
+
+static int waveout_pause(void)
+{
+	if (waveOutPause(wave_out) != MMSYSERR_NOERROR)
+		return -1;
+	return 0;
+}
+
+static int waveout_unpause(void)
+{
+	if (waveOutRestart(wave_out) != MMSYSERR_NOERROR)
+		return -1;
+	return 0;
+}
+
+static int waveout_buffer_space(void)
+{
+	clean_buffers();
+	if (buffer_space == 0)
+		return -1;
+	return buffer_space;
+}
+
+static int waveout_set_option(int key, const char *val)
+{
+	long int ival;
+	int reinit = 0;
+
+	switch (key) {
+	case 0:
+		if (str_to_int(val, &ival) || ival < 4096 || ival > 65536) {
+			errno = EINVAL;
+			return -OP_ERROR_ERRNO;
+		}
+		if (buffers) {
+			waveout_exit();
+			reinit = 1;
+		}
+		buffer_size = ival;
+		break;
+	case 1:
+		if (str_to_int(val, &ival) || ival < 2 || ival > 64) {
+			errno = EINVAL;
+			return -OP_ERROR_ERRNO;
+		}
+		if (buffers) {
+			waveout_exit();
+			reinit = 1;
+		}
+		buffer_count = ival;
+		break;
+	default:
+		return -OP_ERROR_NOT_OPTION;
+	}
+
+	if (reinit) {
+		waveout_init();
+	}
+
+	return 0;
+}
+
+static int waveout_get_option(int key, char **val)
+{
+	switch (key) {
+	case 0:
+		*val = xnew(char, 22);
+		snprintf(*val, 22, "%d", buffer_size);
+		break;
+	case 1:
+		*val = xnew(char, 22);
+		snprintf(*val, 22, "%d", buffer_count);
+		break;
+	default:
+		return -OP_ERROR_NOT_OPTION;
+	}
+	return 0;
+}
+
+const struct output_plugin_ops op_pcm_ops = {
+	.init = waveout_init,
+	.exit = waveout_exit,
+	.open = waveout_open,
+	.close = waveout_close,
+	.write = waveout_write,
+	.pause = waveout_pause,
+	.unpause = waveout_unpause,
+	.buffer_space = waveout_buffer_space,
+	.set_option = waveout_set_option,
+	.get_option = waveout_get_option
+};
+
+const char * const op_pcm_options[] = {
+	"buffer_size",
+	"buffer_count",
+	NULL
+};
+
+const int op_priority = 0;
