Commit Diff


commit - 893f7eed87919d0f7f4e3865207d4fe9556e7ea9
commit + f3cf8697f3f00c468de6b3f03912c97c799b6fa0
blob - caf21c548f295f0990d283b0a6ae722a5557384c
blob + c1084d0128ba152850f016cf6b617b0658e44550
--- .gitignore
+++ .gitignore
@@ -4,6 +4,7 @@ config.h
 config.h.old
 config.log
 config.log.old
+songmeta/songmeta
 web/amused-web
 **/*.d
 **/*.o
blob - 7c788e63407b076082a0a781bdd5ad43f3b4aebc
blob + 74cfd874337d88ae6683d4f7b40cf1d92d18c792
--- Makefile
+++ Makefile
@@ -1,4 +1,4 @@
-.PHONY: all web clean distclean install install-web
+.PHONY: all songmeta web clean distclean install install-songmeta install-web
 
 VERSION =	0.15
 PROG =		amused
@@ -59,11 +59,15 @@ ${PROG}: ${OBJS}
 	${CC} -o $@ ${OBJS} ${LDFLAGS} ${LDADD} ${LDADD_LIB_IMSG} \
 		${LDADD_DECODERS} ${LDADD_LIB_SOCKET} ${LDADD_BACKEND}
 
+songmeta:
+	${MAKE} -C songmeta
+
 web:
 	${MAKE} -C web
 
 clean:
 	rm -f ${OBJS} ${OBJS:.o=.d} ${PROG}
+	-${MAKE} -C songmeta clean
 	-${MAKE} -C web clean
 
 distclean: clean
@@ -75,6 +79,9 @@ install:
 	${INSTALL_PROGRAM} ${PROG} ${DESTDIR}${BINDIR}
 	${INSTALL_MAN} amused.1 ${DESTDIR}${MANDIR}/man1/${PROG}.1
 
+install-songmeta:
+	${MAKE} -C songmeta install
+
 install-web:
 	${MAKE} -C web install
 
@@ -100,6 +107,7 @@ ${DISTNAME}.tar.gz: ${DISTFILES}
 	cd .dist/${DISTNAME} && chmod 755 configure
 	cd .dist/${DISTNAME} && cp -R ../../contrib . && \
 		chmod 755 contrib/amused-monitor
+	${MAKE} -C songmeta DESTDIR=${PWD}/.dist/${DISTNAME}/songmeta dist
 	${MAKE} -C web DESTDIR=${PWD}/.dist/${DISTNAME}/web dist
 	cd .dist && tar zcf ../$@ ${DISTNAME}
 	rm -rf .dist/
blob - /dev/null
blob + 6d8f19b6aef88be092c5a42e6bc5674d2fe1c5ef (mode 644)
--- /dev/null
+++ songmeta/Makefile
@@ -0,0 +1,67 @@
+.PHONY: all clean
+
+PROG =		songmeta
+SRCS =		songmeta.c flac.c id3v1.c id3v2.c ogg.c opus.c text.c \
+		vorbis.c ../compats.c
+
+OBJS =		${SRCS:.c=.o}
+
+DISTFILES =	Makefile songmeta.c flac.c id3v1.c id3v2.c ogg.c opus.c \
+		text.c vorbis.c
+
+all: ${PROG}
+
+../Makefile.configure ../config.h: ../configure ../tests.c
+	@echo "$@ is out of date; please run ../configure"
+	@exit 1
+
+include ../Makefile.configure
+
+# --- targets ---
+
+${PROG}: ${OBJS}
+	${CC} -o $@ ${OBJS} ${LDFLAGS} ${LDADD} ${LDADD_LIB_IMSG} \
+		${LDADD_LIB_SOCKET}
+
+clean:
+	rm -f ${OBJS} ${OBJS:.o=.d} ${PROG}
+
+distclean: clean
+
+install:
+	mkdir -p ${DESTDIR}${BINDIR}
+	mkdir -p ${DESTDIR}${MANDIR}/man1
+	${INSTALL_PROGRAM} ${PROG} ${DESTDIR}${BINDIR}
+#	${INSTALL_MAN} songmeta.1 ${DESTDIR}${MANDIR}/man1/${PROG}.1
+
+install-local:
+	mkdir -p ${HOME}/bin
+	${INSTALL_PROGRAM} ${PROG} ${HOME}/bin
+
+uninstall:
+	rm ${DESTDIR}${BINDIR}/${PROG}
+	rm ${DESTDIR}${MANDIR}/man1/${PROG}.1
+
+.c.o:
+	${CC} -I../ ${CFLAGS} -c $< -o $@
+
+# --- maintainer targets ---
+
+dist:
+	mkdir -p ${DESTDIR}/
+	${INSTALL} -m 0644 ${DISTFILES} ${DESTDIR}/
+
+# --- dependency management ---
+
+# these .d files are produced during the first build if the compiler
+# supports it.
+
+-include flac.d
+-include id3v1.d
+-include id3v2.d
+-include ogg.d
+-include opus.d
+-include songmeta.d
+-include text.d
+-include vorbis.d
+-include ../compats.d
blob - /dev/null
blob + bcb339f7a668484c91922c40ac260bfdc0e0f4f8 (mode 644)
--- /dev/null
+++ songmeta/flac.c
@@ -0,0 +1,116 @@
+/*
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ * FLAC metadata handling.
+ */
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "songmeta.h"
+
+int
+flac_dump(FILE *fp, const char *name, const char *filter)
+{
+	uint8_t		 btype;
+	uint8_t		 magic[4], len[4];
+	size_t		 r, i, n, blen;
+	int		 last = 0;
+
+	if ((r = fread(magic, 1, sizeof(magic), fp)) != sizeof(magic))
+		return (-1);
+
+	if (memcmp(magic, "fLaC", 4) != 0) {
+		warnx("not a flac file %s", name);
+		return (-1);
+	}
+
+	while (!last) {
+		if (fread(&btype, 1, 1, fp) != 1)
+			return (-1);
+		last = btype & 0x80;
+
+		if (fread(len, 1, 3, fp) != 3)
+			return (-1);
+
+		blen = (len[0] << 16)|(len[1] << 8)|len[2];
+
+		if ((btype & 0x07) != 0x04) {
+			//printf("skipping %zu bytes of block type %d\n",
+			//    blen, (btype & 0x07));
+			/* not a vorbis comment, skip... */
+			if (fseeko(fp, blen, SEEK_CUR) == -1)
+				return (-1);
+			continue;
+		}
+
+		if (fread(len, 1, 4, fp) != 4)
+			return (-1);
+
+		/* The vorbis comment has little-endian integers */
+
+		blen = (len[3] << 24)|(len[2] << 16)|(len[1] << 8)|len[0];
+		/* skip the vendor string comment */
+		if (fseeko(fp, blen, SEEK_CUR) == -1)
+			return (-1);
+
+		if (fread(len, 1, 4, fp) != 4)
+			return (-1);
+		n = (len[3] << 24)|(len[2] << 16)|(len[1] << 8)|len[0];
+
+		for (i = 0; i < n; ++i) {
+			char *m, *v;
+
+			if (fread(len, 1, 4, fp) != 4)
+				return (-1);
+			blen = (len[3] << 24)|(len[2] << 16)|(len[1] << 8)|len[0];
+
+			if ((m = malloc(blen + 1)) == NULL)
+				err(1, "malloc");
+
+			if (fread(m, 1, blen, fp) != blen) {
+				free(m);
+				return (-1);
+			}
+			m[blen] = '\0';
+
+			if ((v = strchr(m, '=')) == NULL) {
+				warnx("missing field name!");
+				free(m);
+				return (-1);
+			}
+
+			*v++ = '\0';
+			printf("%s = %s\n", m, v);
+
+			free(m);
+		}
+	}
+
+	return (0);
+}
blob - /dev/null
blob + b35ff6a7117553977876b7b1cca6b7462da0f619 (mode 644)
--- /dev/null
+++ songmeta/id3v1.c
@@ -0,0 +1,127 @@
+/*
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ * ID3v1 and 1.1 handling.
+ */
+
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "songmeta.h"
+
+#define ID3v1_SIZE	128
+
+int
+id3v1_dump(int fd, const char *name, const char *filter)
+{
+	struct stat	 sb;
+	char		*s, *e, id3[ID3v1_SIZE];
+	char		 buf[5]; /* wide enough for YYYY + NUL */
+	ssize_t		 r;
+	off_t		 off;
+
+	if (fstat(fd, &sb) == -1) {
+		warn("fstat %s", name);
+		return (-1);
+	}
+
+	if (sb.st_size < ID3v1_SIZE) {
+		warnx("no id3 section found in %s", name);
+		return (-1);
+	}
+	off = sb.st_size - ID3v1_SIZE;
+	r = pread(fd, id3, ID3v1_SIZE, off);
+	if (r == -1 || r != ID3v1_SIZE) {
+		warn("failed to read id3 section in %s", name);
+		return (-1);
+	}
+
+	s = id3;
+	if (strncmp(s, "TAG", 3) != 0)
+		goto bad;
+	s += 3;
+
+	if (memchr(s, '\0', 30) == NULL)
+		goto bad;
+	if (*s)
+		printfield("title", filter, "Title", 1, s);
+	else if (filter != NULL && matchfield("title", filter))
+		return (-1);
+	s += 30;
+
+	if (memchr(s, '\0', 30) == NULL)
+		goto bad;
+	if (*s)
+		printfield("artist", filter, "Artist", 1, s);
+	else if (filter != NULL && matchfield("artist", filter))
+		return (-1);
+	s += 30;
+
+	if (memchr(s, '\0', 30) == NULL)
+		goto bad;
+	if (*s)
+		printfield("album", filter, "Album", 1, s);
+	else if (filter != NULL && matchfield("album", filter))
+		return (-1);
+	s += 30;
+
+	if (!isdigit((unsigned char)s[0]) ||
+	    !isdigit((unsigned char)s[1]) ||
+	    !isdigit((unsigned char)s[2]) ||
+	    !isdigit((unsigned char)s[3]))
+		goto bad;
+	memcpy(buf, s, 4);
+	buf[4] = '\0';
+	printfield("year", filter, "Year", 0, buf);
+	s += 4;
+
+	if ((e = memchr(s, '\0', 30)) == NULL)
+		goto bad;
+	s += strspn(s, " \t");
+	if (*s)
+		printfield("comment", filter, "Comment", 1, s);
+	else if (filter != NULL && matchfield("comment", filter))
+		return (-1);
+
+	/* ID3v1.1: track number is inside the comment space */
+
+	if (s[28] == '\0' && s[29] != '\0') {
+		snprintf(buf, sizeof(buf), "%d", (unsigned int)s[29]);
+		printfield("track", filter, "Track #", 0, s);
+	} else if (filter != NULL && matchfield("track", filter))
+		return (-1);
+
+	return (0);
+
+ bad:
+	warnx("bad id3 section in %s", name);
+	return (-1);
+}
blob - /dev/null
blob + 5599fa1354c7131188d02d97e403a917d2cb1e4f (mode 644)
--- /dev/null
+++ songmeta/id3v2.c
@@ -0,0 +1,261 @@
+/*
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ * ID3v2(.4.0) handling.
+ */
+
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <endian.h>
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "songmeta.h"
+
+#ifndef nitems
+#define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
+#endif
+
+#define ID3v2_HDR_SIZE		10
+#define ID3v2_FRAME_SIZE	10
+
+#define F_UNSYNC	0x80
+#define F_EXTHDR	0x40
+#define F_EXPIND	0x20
+#define F_FOOTER	0x10
+
+static const struct fnmap {
+	const char	*id;
+	const char	*name;
+	const char	*pretty;
+} map[] = {
+	/* AENC Audio encryption */
+	/* APIC Attached picture */
+	/* ASPI Audio seek point index */
+
+	{ "COMM",	"comment",	"Comment" },
+	/* COMR Commercial frame */
+
+	/* ENCR Encryption method registration */
+	/* EQU2 Equalisation (2) */
+	/* ETCO Event time codes */
+
+	/* GEOB General encapsulated object */
+	/* GRID Group identification registration */
+
+	/* LINK Linked information */
+
+	/* MCDI Music CD identifier */
+	/* MLLT MPEG location lookup table */
+
+	/* OWNE Ownership frame */
+
+	/* PRIV Private frame */
+	/* PCNT Play counter */
+	/* POPM Popularimeter */
+	/* POSS Position synchronisation frame */
+
+	/* RBUF Recommended buffer size */
+	/* RVA2 Relative volume adjustment (2) */
+	/* RVRB Reverb */
+
+	/* SEEK Seek frame */
+	/* SIGN Signature frame */
+	/* SYLT Synchronised lyric text */
+	/* SYTC Synchronised tempo codes */
+
+	{ "TALB",	"album",		"Album" },
+	{ "TBPM",	"bpm",			"beats per minute" },
+	{ "TCOM",	"composer",		"Composer" },
+	{ "TCON",	"content-type",		"Content type" },
+	{ "TCOP",	"copyright-message",	"Copyright message" },
+	{ "TDEN",	"encoding-time",	"Encoding time" },
+	{ "TDLY",	"playlist-delay",	"Playlist delay" },
+	{ "TDOR",	"original-release-time","Original release time" },
+	{ "TDRC",	"recording-time",	"Recording time" },
+	{ "TDRL",	"release-time",		"Release time" },
+	{ "TDTG",	"tagging-time",		"Tagging time" },
+	{ "TENC",	"encoded-by",		"Encoded by" },
+	{ "TEXT",	"lyricist",		"Lyricist/Text writer" },
+	{ "TFLT",	"file-type",		"File type" },
+	{ "TIPL",	"involved-people",	"Involved people list" },
+	{ "TIT1",	"content-group-description", "Content group description" },
+	{ "TIT2",	"title",		"Title" },
+	{ "TIT3",	"subtitle",		"Subtitle" },
+	{ "TKEY",	"initial-key",		"Initial key" },
+	{ "TLAN",	"language",		"Language" },
+	{ "TLEN",	"length",		"Length" },
+	{ "TMCL",	"musician",		"Musician credits list" },
+	{ "TMED",	"media-type",		"Media type" },
+	{ "TMOO",	"mood",			"Mood" },
+	{ "TOAL",	"original-title",	"Original album/movie/show title" },
+	{ "TOFN",	"original-filename",	"Original filename" },
+	{ "TOLY",	"original-lyricist",	"Original lyricist(s)/text writer(s)" },
+	{ "TOPE",	"original-artist",	"Original artist(s)/performer(s)" },
+	{ "TOWN",	"licensee",		"File owner/licensee" },
+	{ "TPE1",	"lead-performer",	"Lead performer(s)/Soloist(s)" },
+	{ "TPE2",	"band",			"Band/orchestra/accompaniment" },
+	{ "TPE3",	"conductor",		"Conductor/performer refinement" },
+	{ "TPE4",	"interpreted-by",	"Interpreted, remixed, or otherwise modified by" },
+	{ "TPOS",	"part",			"Part of a set" },
+	{ "TPRO",	"notice",		"Produced notice" },
+	{ "TPUB",	"publisher",		"Publisher" },
+	{ "TRCK",	"track",		"Track number/Position in set" },
+	{ "TRSN",	"radio-name",		"Internet radio station name" },
+	{ "TRSO",	"radio-owner",		"Internet radio station owner" },
+	{ "TSOA",	"album-order",		"Album sort order" },
+	{ "TSOP",	"performer-order",	"Performer sort order" },
+	{ "TSOT",	"title-order",		"Title sort order" },
+	{ "TSRC",	"isrc",			"ISRC (international standard recording code)" },
+	{ "TSSE",	"encoder",		"Software/Hardware and settings used for encoding" },
+	{ "TSST",	"subtitle",		"Set subtitle" },
+	/* TXXX user defined text information frame */
+
+	/* UFID Unique file identifier */
+	/* USER Terms of use */
+	/* USLT Unsynchronised lyric/text transcription */
+
+	/* WCOM Commercial information */
+	/* WCOP Copyright/legal information */
+	/* WOAF Official audio file webpage */
+	/* WOAR Official artist/performer webpage */
+	/* WOAS Official audio source webpage */
+	/* WORS Official internet radio station homepage */
+	/* WPAY Payment */
+	/* WPUB Publishers official webpage */
+	/* WXXX User defined URL link frame */
+};
+
+static int
+mapcmp(const void *k, const void *e)
+{
+	const struct fnmap *f = e;
+
+	return (memcmp(k, f->id, 4));
+}
+
+static uint32_t
+fromss32(uint32_t x)
+{
+	uint8_t		y[4];
+
+	memcpy(y, &x, sizeof(x));
+	return (y[0] << 21) | (y[1] << 14) | (y[2] << 7) | y[3];
+}
+
+int
+id3v2_dump(int fd, const char *name, const char *filter)
+{
+	struct fnmap	*f;
+	char		 hdr[ID3v2_HDR_SIZE];
+	char		*s;
+	ssize_t		 r;
+	uint8_t		 flags;
+	uint32_t	 size, fsize;
+
+	if ((r = read(fd, hdr, sizeof(hdr))) == -1 ||
+	    r != ID3v2_HDR_SIZE) {
+		warn("read failed: %s", name);
+		return (-1);
+	}
+
+	s = hdr;
+	if (strncmp(s, "ID3", 3))
+		goto bad;
+	s += 3;
+
+	if (s[0] != 0x04 && s[1] != 0x00)
+		goto bad;
+	s += 2;
+
+	flags = s[0];
+	if ((s[0] & 0x0F) != 0)
+		goto bad;
+	s += 1;
+
+#ifdef DEBUG
+	const char *sep = "";
+	printf("flags:\t");
+	if (flags & F_UNSYNC) printf("%sunsync", sep), sep = ",";
+	if (flags & F_EXTHDR) printf("%sexthdr", sep), sep = ",";
+	if (flags & F_EXPIND) printf("%sexpind", sep), sep = ",";
+	if (flags & F_FOOTER) printf("%sfooter", sep), sep = ",";
+	printf(" (0x%x)\n", flags);
+#endif
+
+	if (flags & F_EXPIND) {
+		warnx("don't know how to handle the extended header yet.");
+		return (-1);
+	}
+
+	memcpy(&size, s, 4);
+	size = fromss32(size);
+
+	while (size > 0) {
+		if ((r = read(fd, hdr, sizeof(hdr))) == -1 ||
+		    r != ID3v2_FRAME_SIZE) {
+			warn("read failed: %s", name);
+			return (-1);
+		}
+
+		memcpy(&fsize, hdr + 4, sizeof(fsize));
+		fsize = fromss32(fsize);
+		if (fsize == 0)
+			break;	/* XXX padding?? */
+		if (fsize + 10 > size) {
+			warnx("bad frame length (%d vs %d)", fsize, size);
+			return (-1);
+		}
+		size -= fsize + 10;
+
+		f = bsearch(hdr, map, nitems(map), sizeof(map[0]), mapcmp);
+		if (f == NULL) {
+			if (lseek(fd, fsize, SEEK_CUR) == -1) {
+				warn("lseek");
+				return (-1);
+			}
+			continue;
+		}
+
+		/* XXX skip encoding for now */
+		lseek(fd, SEEK_CUR, 1);
+		fsize--; /* XXX */
+
+		if (readprintfield(f->name, filter, f->pretty,
+		    ENC_UTF8, fd, fsize) == -1)
+			return (-1);
+	}
+
+	return (0);
+
+ bad:
+	warnx("bad ID3v2 section");
+	return (-1);
+}
blob - /dev/null
blob + 0f635293781d5e61d2433298d36c94ee782bb9ae (mode 644)
--- /dev/null
+++ songmeta/ogg.c
@@ -0,0 +1,204 @@
+/*
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ * Ogg file-format handling.
+ */
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "ogg.h"
+
+struct ogg {
+	FILE		*fp;
+	const char	*name;
+	int		 firstpkt;
+	uint32_t	 sn;
+	int		 chained;	/* use only the sn stream */
+	size_t		 plen;
+};
+
+static int
+readpkt(struct ogg *ogg)
+{
+	uint32_t	 sn;
+	uint8_t		 magic[4], ssv, htype, ps, b;
+	size_t		 r;
+	int		 i;
+
+ again:
+	ogg->plen = 0;
+	if ((r = fread(magic, 1, sizeof(magic), ogg->fp)) != sizeof(magic))
+		return (-1);
+
+	if (memcmp(magic, "OggS", 4) != 0) {
+		warnx("not an ogg file: %s", ogg->name);
+		return (-1);
+	}
+
+	if (fread(&ssv, 1, 1, ogg->fp) != 1 ||
+	    fread(&htype, 1, 1, ogg->fp) != 1)
+		return (-1);
+
+	/* skip the absolute granule position */
+	if (fseeko(ogg->fp, 8, SEEK_CUR) == -1)
+		return (-1);
+
+	/* the serial number of the stream */
+	if (fread(&sn, 1, sizeof(sn), ogg->fp) != sizeof(ogg->sn))
+		return (-1);
+	sn = le32toh(sn);
+
+	/* ignore sequence number and crc32 for now */
+	if (fseeko(ogg->fp, 4 + 4, SEEK_CUR) == -1)
+		return (-1);
+
+	if (fread(&ps, 1, 1, ogg->fp) != 1)
+		return (-1);
+
+	ogg->plen = 0;
+	for (i = 0; i < ps; ++i) {
+		if (fread(&b, 1, 1, ogg->fp) != 1)
+			return (-1);
+		ogg->plen += b;
+	}
+
+	/* found some data without a suitable stream */
+	if (!ogg->chained && !(htype & 0x02))
+		return (-1);
+
+	if (ogg->chained && ogg->sn != sn) {
+		/* not "our" stream */
+		if (fseeko(ogg->fp, ogg->plen, SEEK_CUR) == -1)
+			return (-1);
+		ogg->plen = 0;
+		goto again;
+	}
+
+	ogg->sn = sn;
+	return (0);
+}
+
+struct ogg *
+ogg_open(FILE *fp, const char *name)
+{
+	struct ogg	*ogg;
+
+	ogg = calloc(1, sizeof(*ogg));
+	if (ogg == NULL)
+		return (NULL);
+
+	ogg->fp = fp;
+	ogg->name = name;
+	ogg->firstpkt = 1;
+
+	if (readpkt(ogg) == -1) {
+		free(ogg);
+		return (NULL);
+	}
+
+	return (ogg);
+}
+
+size_t
+ogg_read(struct ogg *ogg, void *buf, size_t len)
+{
+	size_t	 r;
+
+	if (len == 0)
+		return (0);
+
+	if (ogg->plen == 0 && readpkt(ogg) == -1)
+		return (0);
+
+	if (len > ogg->plen)
+		len = ogg->plen;
+
+	r = fread(buf, 1, len, ogg->fp);
+	ogg->plen -= r;
+	return (r);
+}
+
+int
+ogg_seek(struct ogg *ogg, off_t n)
+{
+	/* not implemented */
+	if (n < 0)
+		return (-1);
+
+	while (n > 0) {
+		if (ogg->plen == 0 && readpkt(ogg) == -1)
+			return (-1);
+
+		if (n >= ogg->plen) {
+			if (fseeko(ogg->fp, ogg->plen, SEEK_CUR) == -1)
+				return (-1);
+			n -= ogg->plen;
+			ogg->plen = 0;
+			continue;
+		}
+
+		if (fseeko(ogg->fp, n, SEEK_CUR) == -1)
+			return (-1);
+		ogg->plen -= n;
+		break;
+	}
+
+	return (0);
+}
+
+int
+ogg_skip_page(struct ogg *ogg)
+{
+	return (ogg_seek(ogg, ogg->plen));
+}
+
+void
+ogg_use_current_stream(struct ogg *ogg)
+{
+	ogg->chained = 1;
+}
+
+int
+ogg_rewind(struct ogg *ogg)
+{
+	if (fseeko(ogg->fp, 0, SEEK_SET) == -1)
+		return (-1);
+
+	ogg->plen = 0;
+	ogg->firstpkt = 1;
+	ogg->chained = 0;
+	return (0);
+}
+
+void
+ogg_close(struct ogg *ogg)
+{
+	/* it's up to the caller to close ogg->fp */
+	free(ogg);
+}
blob - /dev/null
blob + edc13f631642e8cc72a89b560258307443433700 (mode 644)
--- /dev/null
+++ songmeta/ogg.h
@@ -0,0 +1,38 @@
+/*
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ * Ogg file-format handling.
+ */
+
+struct ogg;
+
+struct ogg	*ogg_open(FILE *, const char *);
+size_t		 ogg_read(struct ogg *, void *, size_t);
+int		 ogg_seek(struct ogg *, off_t);
+int		 ogg_skip_page(struct ogg *);
+void		 ogg_use_current_stream(struct ogg *);
+int		 ogg_rewind(struct ogg *);
+void		 ogg_close(struct ogg *);
blob - /dev/null
blob + cb9c75b9e67fab9f41ba858344a0bb5d7e2b3a95 (mode 644)
--- /dev/null
+++ songmeta/opus.c
@@ -0,0 +1,113 @@
+/*
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <err.h>
+#include <endian.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "ogg.h"
+#include "songmeta.h"
+
+int
+opus_match(struct ogg *ogg)
+{
+	uint8_t		 hdr[8], v;
+
+	if (ogg_read(ogg, hdr, sizeof(hdr)) != sizeof(hdr))
+		return (-1);
+	if (memcmp(hdr, "OpusHead", 8) != 0)
+		return (-1);
+
+	ogg_use_current_stream(ogg);
+
+	if (ogg_read(ogg, &v, 1) != 1)
+		return (-1);
+	if (v < 1 || v > 2) {
+		warnx("unsupported opus version %d", v);
+		return (-1);
+	}
+
+	/* skip the rest of the identification header */
+	if (ogg_skip_page(ogg) == -1)
+		return (-1);
+
+	/* now there should be the optional tag section */
+	if (ogg_read(ogg, hdr, sizeof(hdr)) != sizeof(hdr))
+		return (-1);
+	if (memcmp(hdr, "OpusTags", 8) != 0)
+		return (-1);
+
+	return (0);
+}
+
+int
+opus_dump(struct ogg *ogg, const char *name, const char *filter)
+{
+	static char	 buf[2048]; /* should be enough... */
+	char		*v;
+	uint32_t	 i, n, l, len;
+
+	if (ogg_read(ogg, &len, sizeof(len)) != sizeof(len))
+		return (-1);
+	len = le32toh(len);
+	if (ogg_seek(ogg, +len) == -1)
+		return (-1);
+
+	if (ogg_read(ogg, &n, sizeof(n)) != sizeof(n))
+		return (-1);
+	n = le32toh(n);
+
+	for (i = 0; i < n; ++i) {
+		if (ogg_read(ogg, &len, sizeof(len)) != sizeof(len))
+			return (-1);
+		len = le32toh(len);
+
+		l = len;
+		if (l >= sizeof(buf))
+			l = len - 1;
+		len -= l;
+
+		if (ogg_read(ogg, buf, l) != l ||
+		    ogg_seek(ogg, +len) == -1)
+			return (-1);
+		buf[l] = '\0';
+
+		if ((v = strchr(buf, '=')) == NULL)
+			return (-1);
+		*v++ = '\0';
+
+		/*
+		 * XXX should probably ignore R128_TRACK_GAIN and
+		 * R128_ALBUM_GAIN.
+		 */
+
+		printf("%s = %s\n", buf, v);
+	}
+
+
+	return (-1);
+}
blob - /dev/null
blob + 3465d7f5cfd013054d84cfeec22f70301cc13050 (mode 644)
--- /dev/null
+++ songmeta/songmeta.c
@@ -0,0 +1,223 @@
+/*
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <err.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "ogg.h"
+#include "songmeta.h"
+
+int printraw;
+
+static void __dead
+usage(void)
+{
+	fprintf(stderr, "usage: %s [-r] [-g field] files...\n",
+	    getprogname());
+	exit(1);
+}
+
+int
+matchfield(const char *field, const char *filter)
+{
+	if (filter == NULL)
+		return (1);
+	return (strcasecmp(field, filter) == 0);
+}
+
+int
+printfield(const char *field, const char *filter, const char *fname,
+    int enc, const char *str)
+{
+	if (!matchfield(field, filter))
+		return (0);
+
+	if (filter == NULL) {
+		printf("%s:\t", fname);
+		if (strlen(fname) < 8)
+			printf("\t");
+	}
+
+	if (enc == ENC_GUESS) {
+		mlprint(str);
+		puts("");
+	} else
+		printf("%s\n", str);
+
+	return (0);
+}
+
+int
+readprintfield(const char *field, const char *filter, const char *fname,
+    int enc, int fd, off_t len)
+{
+	static char	 buf[BUFSIZ + 1];
+	size_t		 n;
+	ssize_t		 r;
+
+	if (!matchfield(field, filter))
+		return (0);
+
+	if (filter == NULL) {
+		printf("%s:\t", fname);
+		if (strlen(fname) < 8)
+			printf("\t");
+	}
+
+	while (len > 0) {
+		if ((n = len) > sizeof(buf) - 1)
+			n = sizeof(buf) - 1;
+
+		if ((r = read(fd, buf, n)) == -1) {
+			warn("read");
+			return (-1);
+		}
+		if (r == 0) {
+			warnx("unexpected EOF");
+			return (-1);
+		}
+		buf[r] = '\0';
+
+		if (enc == ENC_GUESS)
+			mlprint(buf);
+		else
+			fwrite(buf, 1, r, stdout);
+
+		len -= r;
+	}
+
+	puts("");
+	return (0);
+}
+
+static int
+dofile(FILE *fp, const char *name, const char *filter)
+{
+	static char	 buf[512];
+	struct ogg	*ogg;
+	size_t		 r, ret = -1;
+
+	if ((r = fread(buf, 1, sizeof(buf), fp)) < 8) {
+		warn("failed to read %s", name);
+		return (-1);
+	}
+
+	if (fseek(fp, 0, SEEK_SET) == -1) {
+		warn("fseek failed in %s", name);
+		return (-1);
+	}
+
+	if (memcmp(buf, "fLaC", 4) == 0)
+		return flac_dump(fp, name, filter);
+
+	if (memcmp(buf, "ID3", 3) == 0)
+		return id3v2_dump(fileno(fp), name, filter);
+
+	/* maybe it's an ogg file */
+	if ((ogg = ogg_open(fp, name)) != NULL) {
+		if (vorbis_match(ogg) != -1) {
+			ret = vorbis_dump(ogg, name, filter);
+			ogg_close(ogg);
+			return (ret);
+		}
+
+		if (ogg_rewind(ogg) == -1) {
+			warn("I/O error on %s", name);
+			ogg_close(ogg);
+			return (-1);
+		}
+
+		if (opus_match(ogg) != -1) {
+			ret = opus_dump(ogg, name, filter);
+			ogg_close(ogg);
+			return (ret);
+		}
+		ogg_close(ogg);
+	}
+
+	if (ferror(fp)) {
+		warn("I/O error on %s", name);
+		return (-1);
+	}
+
+	/* TODO: id3v1? */
+
+	warnx("unknown file format: %s", name);
+	return (-1);
+}
+
+int
+main(int argc, char **argv)
+{
+	const char	*filter = NULL;
+	FILE		*fp;
+	int		 ch;
+	int		 ret = 0;
+
+	if (pledge("stdio rpath", NULL) == -1)
+		err(1, "pledge");
+
+	while ((ch = getopt(argc, argv, "g:r")) != -1) {
+		switch (ch) {
+		case 'g':
+			filter = optarg;
+			break;
+		case 'r':
+			printraw = 1;
+			break;
+		default:
+			usage();
+		}
+	}
+	argc -= optind;
+	argv += optind;
+
+	if (argc == 0)
+		usage();
+
+	for (; *argv; ++argv) {
+		if ((fp = fopen(*argv, "r")) == NULL) {
+			warn("can't open %s", *argv);
+			ret = 1;
+			continue;
+		}
+
+		if (argc != 1)
+			printf("=> %s\n", *argv);
+		if (dofile(fp, *argv, filter) == -1)
+			ret = 1;
+
+		fclose(fp);
+	}
+
+	return (ret);
+}
blob - /dev/null
blob + e756735ddcceba0521a46110a9cbe161519855b9 (mode 644)
--- /dev/null
+++ songmeta/songmeta.h
@@ -0,0 +1,60 @@
+/*
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+extern int printraw;
+
+enum {
+	ENC_GUESS,
+	ENC_UTF8,
+};
+
+struct ogg;
+
+/* flac.c */
+int	 flac_dump(FILE *, const char *, const char *);
+
+/* id3v1.c */
+int	 id3v1_dump(int, const char *, const char *);
+
+/* id3v2.c */
+int	 id3v2_dump(int, const char *, const char *);
+
+/* opus.c */
+int	 opus_match(struct ogg *);
+int	 opus_dump(struct ogg *, const char *, const char *);
+
+/* vorbis.c */
+int	 vorbis_match(struct ogg *);
+int	 vorbis_dump(struct ogg *, const char *, const char *);
+
+/* songmeta.c */
+int	 matchfield(const char *, const char *);
+int	 printfield(const char *, const char *, const char *, int,
+	    const char *);
+int	 readprintfield(const char *, const char *, const char *,
+	    int, int, off_t);
+
+/* text.c */
+void	 mlprint(const char *);
blob - /dev/null
blob + 7ecd534e9c131e64ac276813a1ce5fd512b11391 (mode 644)
--- /dev/null
+++ songmeta/text.c
@@ -0,0 +1,84 @@
+/*
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "../config.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <wchar.h>
+
+#include "songmeta.h"
+
+/*
+ * The idea is to try to decode the text as UTF-8; if it fails assume
+ * it's ISO Latin.  Some strings in ISO Latin may be decoded correctly
+ * as UTF-8 (since both are a superset of ASCII).  In every case, this
+ * is just a "best effort" for when we don't have other clues about
+ * the format.
+ */
+static int
+u8decode(const char *s, int print)
+{
+	wchar_t	 wc;
+	int	 len;
+
+	for (; *s != '\0'; s += len) {
+		if ((len = mbtowc(&wc, s, MB_CUR_MAX)) == -1) {
+			(void)mbtowc(NULL, NULL, MB_CUR_MAX);
+			return (0);
+		}
+		if (print) {
+			if (!printraw && wcwidth(wc) == -1)
+				putchar('?');
+			else
+				fwrite(s, 1, len, stdout);
+		}
+	}
+
+	return (1);
+}
+
+/* Print a string that may be encoded as ISO Latin. */
+void
+mlprint(const char *s)
+{
+	wchar_t	 wc;
+
+	if (u8decode(s, 0)) {
+		u8decode(s, 1);
+		return;
+	}
+
+	/* let's hope it's ISO Latin */
+
+	while (*s) {
+		/* the first 256 UNICODE codepoints map 1:1 ISO Latin */
+		wc = *s++;
+		if (!printraw && wcwidth(wc) == -1)
+			putchar('?');
+		else
+			fwprintf(stdout, L"%c", wc);
+	}
+}
blob - /dev/null
blob + 224b1f7b9cf83a08022e709cb3a4714c27215eae (mode 644)
--- /dev/null
+++ songmeta/vorbis.c
@@ -0,0 +1,111 @@
+/*
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <endian.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "ogg.h"
+#include "songmeta.h"
+
+int
+vorbis_match(struct ogg *ogg)
+{
+	uint8_t		 hdr[7]; /* packet type + "vorbis" */
+
+	if (ogg_read(ogg, hdr, 7) != 7)
+		return (-1);
+
+	if (memcmp(hdr + 1, "vorbis", 6) != 0)
+		return (-1);
+
+	ogg_use_current_stream(ogg);
+
+	/*
+	 * vorbis version (4 bytes)
+	 * channels (1 byte)
+	 * sample rate (4 bytes)
+	 * bitrate max/nominal/min (4 bytes each)
+	 * blocksize_{0,1} (1 byte)
+	 * framing flag (1 bit -- rounded to 1 byte)
+	 */
+	/* XXX check that the framing bit is 1? */
+	if (ogg_seek(ogg, +23) == -1)
+		return (-1);
+
+	return (0);
+}
+
+int
+vorbis_dump(struct ogg *ogg, const char *name, const char *filter)
+{
+	static char	 buf[2048]; /* should be enough... */
+	char		*v;
+	uint32_t	 i, n, l, len;
+	uint8_t		 pktype, hdr[7];
+
+	if (ogg_read(ogg, hdr, 7) != 7)
+		return (-1);
+	if (memcmp(hdr + 1, "vorbis", 6) != 0)
+		return (-1);
+
+	pktype = hdr[0];
+	if (pktype != 3) /* metadata */
+		return (-1);
+
+	if (ogg_read(ogg, &len, sizeof(len)) != sizeof(len))
+		return (-1);
+	len = le32toh(len);
+	if (ogg_seek(ogg, +len) == -1)
+		return (-1);
+
+	if (ogg_read(ogg, &n, sizeof(n)) != sizeof(n))
+		return (-1);
+	n = le32toh(n);
+
+	for (i = 0; i < n; ++i) {
+		if (ogg_read(ogg, &len, sizeof(len)) != sizeof(len))
+			return (-1);
+		len = le32toh(len);
+
+		l = len;
+		if (l >= sizeof(buf))
+			l = len - 1;
+		len -= l;
+
+		if (ogg_read(ogg, buf, l) != l ||
+		    ogg_seek(ogg, +len) == -1)
+			return (-1);
+		buf[l] = '\0';
+
+		if ((v = strchr(buf, '=')) == NULL)
+			return (-1);
+		*v++ = '\0';
+
+		printf("%s = %s\n", buf, v);
+	}
+			
+	return (0);
+}