/* metadata.c - Management of file metadata
 *
 * Copyright (C) 2005, 2006, 2007  Oskar Liljeblad
 *
 * 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 3 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 Library General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

#include <config.h>
#ifdef HAVE_ID3LIB
#include <id3.h>                /* id3lib */
#undef false /* id3lib bug - it shouldn't mess with false and true! */
#undef true
#endif
#ifdef HAVE_TAGLIB
#include <tag_c.h>
#undef BOOL /* conflicts with libupnp */
#endif
#include <stdbool.h>            /* Gnulib, C99 */
#include <stdint.h>		/* Gnulib, C99 */
#include <dirent.h>		/* POSIX */
#include <sys/stat.h>		/* POSIX */
#include <sys/types.h>		/* POSIX */
#include <fcntl.h>		/* POSIX */
#include <unistd.h>		/* POSIX */
#include <ctype.h>		/* C89 */
#include <inttypes.h>		/* ? */
#include <iconv.h>		/* POSIX */
#include <magic.h>		/* libmagic */
#include <upnp/ithread.h>	/* libupnp */
#include "striconv.h"		/* Gnulib */
#include "gettext.h"		/* Gnulib/gettext */
#define _(s) gettext(s)
#define N_(s) gettext_noop(s)
#include "full-read.h"		/* Gnulib */
#include "xalloc.h"		/* Gnulib */
#include "xvasprintf.h"		/* Gnulib */
#include "dirname.h"		/* Gnulib */
#include "xstrndup.h"		/* Gnulib */
#include "quote.h"		/* Gnulib */
#include "quotearg.h"		/* Gnulib */
#include "minmax.h"		/* Gnulib */
#include "getnline.h"		/* Gnulib */
#include "strbuf.h"
#include "strutil.h"
#include "intutil.h"
#include "gmediaserver.h"

#define DEFAULT_ENTRIES_SIZE 		512
#define DEFAULT_FILE_TYPES 		"mp3,wma,m3u,pls"
#define ROOT_ENTRY_NAME 		"(root)"
#define DEFAULT_PLAYLIST_ENTRIES	16
#define MAX_INVALID_M3U_FILES		16
#define MAX_INVALID_EXTM3U_FILES	16
#define MAX_INVALID_PLS_FILES		16
#define MAX_INVALID_PLS_LINES		16
#define EXT_INFO_DEFAULT		"DLNA.ORG_PS=1;DLNA.ORG_CI=0;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00000000000000000000000000000000"
#define EXT_INFO_PN			";DLNA.ORG_PN="

typedef enum {
    FILE_MP3,
    FILE_MP3_ID3,
    FILE_WMA,
    FILE_RIFF_WAVE,
    FILE_M4A,
    FILE_OGG,
    FILE_MPG,
    FILE_MP4,
    FILE_PLS,
    FILE_M3U,
    FILE_EXTM3U,
    FILE_BMP,
    FILE_GIF,
    FILE_JPG,
    FILE_PNG,
    FILE_TIFF,
    FILE_UNKNOWN,
    FILE_TYPES_COUNT,
} FileType;

typedef struct _InodeList InodeList;

struct _InodeList {
    InodeList *prev;
    ino_t node;
};

/*struct {
    char *mime_type;
    char *names;
    char *descs;
    ItemClass item_class;
    char *dlna_pn;
} file_types[];*/

static const char *file_type_dlna_pn[] = {
    [FILE_MP3] 		= "MP3",
    [FILE_MP3_ID3]	= "MP3",
    [FILE_WMA] 		= "WMAFULL",
    [FILE_RIFF_WAVE] 	= NULL,
    [FILE_M4A] 		= NULL,
    [FILE_OGG]		= NULL,
    [FILE_MPG] 		= NULL,
    [FILE_MP4] 		= NULL,
    [FILE_PLS] 		= NULL,
    [FILE_M3U] 		= NULL,
    [FILE_EXTM3U] 	= NULL,
    [FILE_BMP]		= NULL,
    [FILE_GIF]		= NULL,
    [FILE_JPG]		= "JPEG_TN",
    [FILE_PNG]		= NULL,
    [FILE_TIFF]		= NULL,
    [FILE_UNKNOWN] 	= NULL,
};

static const char *file_type_mime_types[] = {
    [FILE_MP3] 		= "audio/mpeg",
    [FILE_MP3_ID3]	= "audio/mpeg",
    [FILE_WMA] 		= "audio/x-ms-wma",
    [FILE_RIFF_WAVE] 	= "audio/x-wav",
    [FILE_M4A] 		= "audio/mp4",
    [FILE_OGG]		= "audio/vorbis",
    [FILE_MPG] 		= "video/mpeg",
    [FILE_MP4] 		= "video/mp4",
    [FILE_PLS] 		= "audio/x-scpls",
    [FILE_M3U] 		= "audio/m3u",
    [FILE_EXTM3U] 	= "audio/m3u",
    [FILE_BMP]		= "image/bmp",
    [FILE_GIF]		= "image/gif",
    [FILE_JPG]		= "image/jpeg",
    [FILE_PNG]		= "image/png",
    [FILE_TIFF]		= "image/tiff",
    [FILE_UNKNOWN] 	= "application/octet-stream",
};

static const char *file_type_names[] = {
    [FILE_MP3] 		= "mp3",
    [FILE_MP3_ID3]	= "mp3", /* possibly id3mp3 or mp3id3 in the future */
    [FILE_WMA] 		= "wma",
    [FILE_RIFF_WAVE] 	= "wav",
    [FILE_M4A] 		= "m4a",
    [FILE_OGG]		= "ogg",
    [FILE_MPG] 		= "mpg",
    [FILE_MP4] 		= "mp4",
    [FILE_PLS] 		= "pls",
    [FILE_M3U] 		= "m3u",
    [FILE_EXTM3U] 	= "m3u", /* possibly extm3u in the future */
    [FILE_BMP]		= "bmp",
    [FILE_GIF]		= "gif",
    [FILE_JPG]		= "jpg",
    [FILE_PNG]		= "png",
    [FILE_TIFF]		= "tiff",
    [FILE_UNKNOWN] 	= "unknown",
};

/* FIXME: handle translation for these? */
static const char *file_type_descs[] = {
    [FILE_MP3] 		= "MPEG-1 Audio Layer 3 (MP3)",
    [FILE_MP3_ID3]	= "MPEG-1 Audio Layer 3 (MP3) with leading ID3 tag",
    [FILE_WMA] 		= "Windows Media Audio (WMA)",
    [FILE_RIFF_WAVE] 	= "RIFF Wave",
    [FILE_M4A] 		= "MPEG v4 Audio (M4A)",
    [FILE_OGG]		= "Ogg Vorbis audio",
    [FILE_MPG] 		= "MPEG v1/2 Video (MPG)",
    [FILE_MP4] 		= "MPEG v4 Video (MP4)",
    [FILE_PLS]		= "PLS playlist",
    [FILE_M3U]		= "Simple M3U playlist",
    [FILE_EXTM3U]	= "Extended M3U playlist",
    [FILE_BMP]		= "BMP image",
    [FILE_GIF]		= "GIF image",
    [FILE_JPG]		= "JPEG image",
    [FILE_PNG]		= "PNG image",
    [FILE_TIFF]		= "TIFF image",
};

static ItemClass file_type_item_classes[] = {
    [FILE_MP3]         = ITEM_AUDIO,
    [FILE_MP3_ID3]     = ITEM_AUDIO,
    [FILE_WMA]         = ITEM_AUDIO,
    [FILE_RIFF_WAVE]   = ITEM_AUDIO,
    [FILE_M4A]         = ITEM_AUDIO,
    [FILE_OGG]         = ITEM_AUDIO,
    [FILE_MPG]         = ITEM_VIDEO,
    [FILE_MP4]         = ITEM_VIDEO,
    [FILE_PLS]         = ITEM_PLAYLIST,
    [FILE_M3U]         = ITEM_PLAYLIST,
    [FILE_EXTM3U]      = ITEM_PLAYLIST,
    [FILE_BMP]         = ITEM_IMAGE,
    [FILE_GIF]         = ITEM_IMAGE,
    [FILE_JPG]         = ITEM_IMAGE,
    [FILE_PNG]         = ITEM_IMAGE,
    [FILE_TIFF]        = ITEM_IMAGE,
};

static Entry *scan_entry(const char *fullpath, const char *name, int32_t parent, int indent_size, InodeList *inl);
static Entry *scan_playlist_file(const char *fullpath, const char *name, int32_t parent, FileType type, int indent_size, InodeList *inl, ino_t node);

static Entry *root_entry;
static uint32_t entry_count = 0;
static uint32_t entries_size = 0;
static Entry **entries;
#ifdef HAVE_ID3LIB
static iconv_t iso8859_1_to_utf8;
static iconv_t utf16_to_utf8;
#endif
static magic_t magic_cookie;
char *file_types = DEFAULT_FILE_TYPES;
static ithread_mutex_t metadata_mutex;
bool tags_enabled = true;
#ifdef HAVE_ID3LIB
static ID3Tag *id3;
#endif

EntryDetail *
get_entry_detail(const Entry *entry, EntryDetailType type)
{
    EntryDetail *d;
    
    for (d = entry->details; d != NULL; d = d->next) {
	if (d->type == type)
	    return d;
    }

    return NULL;
}

bool
has_entry_detail(const Entry *entry, EntryDetailType type)
{
    return get_entry_detail(entry, type) != NULL;
}

static EntryDetail *
add_entry_detail(Entry *entry, EntryDetailType type)
{
    EntryDetail *detail;

    detail = xmalloc(sizeof(EntryDetail));
    detail->next = entry->details;
    detail->type = type;
    entry->details = detail;

    return detail;
}

static Entry *
make_entry(const char *name, int32_t parent, bool directory)
{
    Entry *entry;

    entry = xmalloc(sizeof(Entry));
    entry->id = entry_count++;
    entry->parent = parent;
    entry->name = xstrdup(name);
    entry->details = NULL;

    if (directory) {
	EntryDetail *detail;
	detail = add_entry_detail(entry, DETAIL_CHILDREN);
	detail->data.children.count = 0;
	detail->data.children.list = NULL;
    }

    if (entry->id >= entries_size) {
        entries_size *= 2;
        entries = xrealloc(entries, entries_size * sizeof(Entry *));
    }
    entries[entry->id] = entry;
    
    return entry;
}

Entry *
get_entry_by_id(uint32_t id)
{
    if (id < 0 || id >= entry_count)
        return NULL;

    return entries[id];
}


static bool
attempt_read_at(int fd, size_t size, off_t pos, uint8_t *buf, const char *fullpath)
{
    size_t count;

    if (lseek(fd, pos, SEEK_SET) != pos) {
	warn(_("%s: cannot seek: %s\n"), quotearg(conv_filename(fullpath)), errstr);
	return false;
    }

    count = full_read(fd, buf, size);
    if (count < size) {
	if (errno != 0)
	    warn(_("%s: cannot read: %s\n"), quotearg(conv_filename(fullpath)), errstr);
	return false;
    }
    return true;
}

static FileType
check_file_content_type(const char *fullpath)
{
    const char *magic;
    int fd;
    uint8_t buf[11];
    int c;

    magic = magic_file(magic_cookie, fullpath);
    if (magic == NULL) {
	warn(_("%s: cannot identify file type: %s\n"), quotearg(conv_filename(fullpath)), magic_error(magic_cookie));
	return FILE_UNKNOWN;
    }

    if (strcmp(magic, "application/octet-stream") != 0
	    && strncmp(magic, "text/plain", 10) != 0) {
	struct {
	    FileType id;
	    char *mime;
	} mime_map[] = {
	    { FILE_MP3, 	"audio/mpeg" }, /* XXX: FILE_MP3_ID3? */
	    { FILE_WMA, 	"audio/x-ms-wma" },
	    { FILE_RIFF_WAVE,	"audio/x-wav" },
	    { FILE_M4A,		"audio/mp4" },
	    { FILE_OGG,		"application/ogg" },
	    { FILE_MPG,		"video/mpeg" },
	    { FILE_MPG,		"video/mpv" },
	    { FILE_MPG,		"video/mp2p" },
	    { FILE_MPG,		"video/mp2t" },
	    { FILE_MP4,		"video/mp4" },
	    { FILE_MP4,		"video/mp4v-es" },
	    { FILE_MP4,		"video/h264" },
	    { FILE_MP4,		"video/3gpp" },
	    { FILE_BMP,		"image/bmp" },
	    { FILE_GIF,		"image/gif" },
	    { FILE_JPG,		"image/jpeg" },
	    { FILE_PNG,		"image/png" },
	    { FILE_TIFF,	"image/tiff" },
            { 0, },
        };
        for (c = 0; mime_map[c].mime != NULL; c++) {
            if (strcmp(magic, mime_map[c].mime) == 0)
                return mime_map[c].id;
        }
    }

    fd = open(fullpath, O_RDONLY);
    if (fd < 0) {
        warn(_("%s: cannot open for reading: %s\n"), quotearg(conv_filename(fullpath)), errstr);
        return FILE_UNKNOWN;
    }
    if (!attempt_read_at(fd, 11, 0, buf, fullpath)) {
        close(fd);
        return FILE_UNKNOWN;
    }

    /* Microsoft ASF */
    if (buf[0] == 0x30 && buf[1] == 0x26 && buf[2] == 0xb2 && buf[3] == 0x75) {
        close(fd); /* Ignore errors since we opened for reading */
        return FILE_WMA;
    }


    /* Extended M3U */
    if (buf[0]=='#' && buf[1]=='E' && buf[2]=='X' && buf[3]=='T' && buf[4]=='M' && buf[5]=='3' && buf[6]=='U' && (buf[7]=='\r' || buf[7]=='\n')) {
        close(fd); /* Ignore errors since we opened for reading */
        return FILE_EXTM3U;
    }
    /* Playlist (PLS) */
    if (memcmp(buf, "[playlist]", 10) == 0 && (buf[10] == '\r' || buf[10] == '\n')) {
        close(fd); /* Ignore errors since we opened for reading */
        return FILE_PLS;
    }
    /* Simple M3U */
    if (ends_with_nocase(fullpath, ".m3u")) {
        close(fd); /* Ignore errors since we opened for reading */
        return FILE_M3U;
    }

    close(fd); /* Ignore errors since we opened for reading */
    return FILE_UNKNOWN;
}

#ifdef HAVE_ID3LIB
/* XXX: move this function to a more appropriate place, perhaps lib/striconv? */
static char *
iconv_alloc_term(iconv_t *cd, const char *inbuf, size_t inbytes_remaining, size_t nullterm)
{
    /* FIXME: use this instead */
    /*char *outbuf;
    size_t outbuf_size;
    int err;

    err = mem_cd_iconv(inbuf, inbytes_remaining, *cd, &outbuf, &outbuf_size);
    if (err == 0) {
      if (nullterm != 0) {
        xrealloc(outbuf, outbuf_size+nullterm);
        memset(outbuf+outbuf_size, 0, nullterm);
      }
    }
    return err;*/



    char *dest;
    char *outp;
    /* Guess the maximum length the output string can have.  */
    size_t outbuf_size = inbytes_remaining + nullterm;
    size_t outbytes_remaining;
    size_t err;
    bool have_error = false;

    /* Use a worst-case output size guess, so long as that wouldn't be
       too large for comfort.  It's OK if the guess is wrong so long as
       it's nonzero.  */
    size_t approx_sqrt_SIZE_MAX = SIZE_MAX >> (sizeof (size_t) * CHAR_BIT / 2);
    if (outbuf_size <= approx_sqrt_SIZE_MAX / MB_LEN_MAX)
      outbuf_size *= MB_LEN_MAX;
    outbytes_remaining = outbuf_size - nullterm;

    outp = dest = xmalloc(outbuf_size);

again:
    err = iconv(cd, (char **) &inbuf, &inbytes_remaining, &outp, &outbytes_remaining);

    if (err == (size_t) -1) {
        switch (errno) {
        case EINVAL:
            /* Incomplete text, do not report an error */
            break;
        case E2BIG: {
            size_t used = outp - dest;
            size_t newsize = outbuf_size * 2;
            char *newdest;

            if (newsize <= outbuf_size) {
                errno = ENOMEM;
                have_error = true;
                goto out;
            }
            newdest = xrealloc(dest, newsize);
            dest = newdest;
            outbuf_size = newsize;

            outp = dest + used;
            outbytes_remaining = outbuf_size - used - nullterm;

            goto again;
        }
        case EILSEQ:
            have_error = true;
            break;
        default:
            have_error = true;
            break;
        }
    }

    for (; nullterm > 0; nullterm--)
        *outp++ = '\0';

out:
    if (have_error && dest != NULL) {
        int save_errno = errno;
        free(dest);
        errno = save_errno;
        dest = NULL;
    }

    return dest;
}

static char *
id3_frame_name(ID3_FrameID frame_id)
{
    switch (frame_id) {
    case ID3FID_TITLE:
        return _("title");
    case ID3FID_ALBUM:
        return _("album");
    case ID3FID_LEADARTIST:
        return _("artist");
    case ID3FID_CONTENTTYPE:
        return _("genre");
    case ID3FID_TRACKNUM:
        return _("track number");
    default:
        return _("unknown frame");
    }
}

static char *
get_id3_string(const char *filename, ID3Tag *id3tag, ID3_FrameID frame_id)
{
    ID3Frame *frame;
    ID3Field *field;

    frame = ID3Tag_FindFrameWithID(id3tag, frame_id);
    if (frame != NULL) {
	field = ID3Frame_GetField(frame, ID3FN_TEXT);
	if (field != NULL) {
	    size_t insize;
	    char *inbuf;
	    char *outbuf = NULL;

	    insize = ID3Field_Size(field);
	    if (insize == 0)
	        return xstrdup("");
	    inbuf = xmalloc(insize*2);
	    if (ID3Field_GetASCII(field, inbuf, insize) > 0) {
                outbuf = iconv_alloc_term(iso8859_1_to_utf8, inbuf, insize, 1);
                if (outbuf == NULL)
                    warn(_("%s: cannot convert ID3 %s tag text: %s\n"), quotearg(conv_filename(filename)), id3_frame_name(frame_id), errstr);
                /*outbuf = xstrndup(inbuf, insize);*/
	    }
	    /* insize/sizeof(unicode_t) isn't very nice here. But unfortunately it is necessary,
	     * probably because of a bug in id3lib 3.8.3:
	     *
             * size_t ID3_FieldImpl::Get(unicode_t *buffer, size_t maxLength)
             *    size_t size = this->Size();                                  [size in bytes]
             *    length = dami::min(maxLength, size);                         [wrong!]
             *    ::memcpy((void *)buffer, (void *)_text.data(), length * 2);  [wrong!]
             * [..]
             */
	    else if (ID3Field_GetUNICODE(field, (unicode_t *) inbuf, insize/sizeof(unicode_t)) > 0) {
                outbuf = iconv_alloc_term(utf16_to_utf8, inbuf, insize, 1);
                if (outbuf == NULL) {
                    warn(_("%s: cannot convert ID3 %s tag text: %s\n"), quotearg(conv_filename(filename)), id3_frame_name(frame_id), errstr);
                } else {
                    /* Get rid of UTF-8 BOM (see http://en.wikipedia.org/wiki/Byte_Order_Mark), */
                    /* which is inserted by iconv if there's an UTF-16 LE/BE BOM in input text. */
                    if (outbuf[0] == (char)0xEF && outbuf[1] == (char)0xBB && outbuf[2] == (char)0xBF)
	                memmove(outbuf, outbuf+3, strlen(outbuf+3)+1);
                }
            }
            free(inbuf);
            return outbuf;
	}
    }

    return NULL;
}
#endif

static bool
inode_in_list(ino_t node, InodeList *inl)
{
    for (; inl != NULL; inl = inl->prev) {
        if (inl->node == node)
            return true;
    }
    return false;
}

static bool
add_playlist_child(Entry *parent, size_t *children_size, char *buf, const char *playlist_dir, int indent_size, InodeList *inl)
{
    Entry *child;
    char *child_path = buf;
    char *child_name;

    if (buf[0] != '/' && strncasecmp(buf, "http://", 7) != 0)
        child_path = concat_filenames(playlist_dir, buf);
    /* XXX: using base_name here may not be entire appropriate -
     * maybe child_path is an url?
     */

    child_name = xstrdup(conv_filename(base_name(child_path)));
    child = scan_entry(child_path, child_name, parent->id, indent_size, inl);
    free(child_name);
    if (child != NULL) {
        EntryChildren *children;

        children = &(get_entry_detail(parent, DETAIL_CHILDREN)->data.children);
        if (children->list == NULL) {
            children->list = xmalloc((*children_size) * sizeof(int32_t));
        } else if (children->count >= (*children_size)) {
            *children_size *= 2;
            children->list = xrealloc(children->list, (*children_size) * sizeof(int32_t));
        }
        children->list[children->count++] = child->id;
    }
    if (child_path != buf)
        free(child_path);

    return child != NULL;
}

static bool
fix_playlist_path(char *path)
{
    char *p;

    /* Deny empty and absolute paths */
    if (path[0] == '\0' || path[0] == '/' || path[0] == '\\')
        return false;

    /* Convert backslashes to slashes. Deny '..' at the same time. */
    p = path;
    for (; *path; path++) {
        if (*path == '\\' || *path == '/') {
            *path = '/';
            if (path-p == 2 && p[0] == '.' && p[1] == '.')
                return false;
            p = path+1;
        }
    }
    if (path-p == 2 && p[0] == '.' && p[1] == '.')
        return false;

    return true;
}

static Entry *
scan_playlist_file(const char *fullpath, const char *name, int32_t parent, FileType type, int indent_size, InodeList *inl, ino_t node)
{
    FILE *fh;
    size_t alloc_max;
    char *buf = NULL;
    size_t bufsize = 0;
    ssize_t len;
    Entry *entry;
    char *playlist_dir;
    size_t children_size = DEFAULT_PLAYLIST_ENTRIES; /* just some basic initial size */
    int ln;
    int bad = 0;
    char *indent;
    InodeList inl_new;

    indent = xstrdupn(" ", indent_size*2);

    if (inode_in_list(node, inl)) {
        warn(_("%s: recursive loop detected - ignoring\n"), quotearg(conv_filename(fullpath)));
        free(indent);
        return NULL;
    }

    say(4, _("%sScanning playlist %s\n"), indent, quote(conv_filename(fullpath)));

    fh = fopen(fullpath, "r");
    if (fh == NULL) {
        warn(_("%s: cannot open file for reading: %s\n"), quotearg(conv_filename(fullpath)), errstr);
        free(indent);
        return NULL;
    }

    inl_new.prev = inl;
    inl_new.node = node;
    playlist_dir = dir_name(fullpath);
    entry = make_entry(name, parent, true);

    /* Note: we don't handle playlists with MAC-like newlines (CR only),
     * simply because getline or getdelim doesn't handle that easily.
     */
    alloc_max = MAX(FILENAME_MAX+3, FILENAME_MAX); /* to handle overflow */
    if (type == FILE_PLS || type == FILE_EXTM3U) {
        /* We know the first line is OK, we tested that in
         * check_file_content_type. The read can still fail,
         * that's why we test using ferror below.
         */
        getnline(&buf, &bufsize, alloc_max, fh);
        ln = 2;
    } else {
        ln = 1;
    }
    if (!ferror(fh)) {
        for (; (len = getnline(&buf, &bufsize, alloc_max, fh)) >= 0; ln++) {
            len = chomp(buf, len);
            if (type == FILE_PLS) {
                if (len >= 7 && strncasecmp(buf, "Title", 5) == 0) {
                    /* Ignore Title lines. */
                } else if (len >= 8 && strncasecmp(buf, "Length", 6) == 0) {
                    /* Ignore Length lines. */
                } else if (len >= 17 && strncasecmp(buf, "NumberOfEntries=", 16) == 0) {
                    /* Ignore NumberOfEntries lines. */
                } else if (len >= 9 && strncasecmp(buf, "Version=", 8) == 0) {
                    /* Ignore Version lines. */
                } else if (len >= 7 && strncasecmp(buf, "File", 4) == 0) {
                    int c;

                    for (c = 4; c < len && buf[c] >= '0' && buf[c] <= '9'; c++);
                    if (c == 4 || c >= len || buf[c] != '=' || !fix_playlist_path(buf+c+1)) {
                        warn(_("%s: invalid line %d\n"), quotearg(conv_filename(fullpath)), ln);
                        break;
                    }
                    /* Ignore the numbering of files in PLS playlists. */
                    if (!add_playlist_child(entry, &children_size, buf+c+1, playlist_dir, indent_size+1, &inl_new)) {
                        bad++;
                        if (bad > MAX_INVALID_PLS_FILES) {
                            /* For now, just break out of the loop. */
                            warn(_("%s: too many invalid files (max allowed %d)\n"), quotearg(conv_filename(fullpath)), MAX_INVALID_PLS_FILES);
                            break;
                        }
                    }
                } else {
                    warn(_("%s: invalid line %d\n"), quotearg(conv_filename(fullpath)), ln);
                    bad++;
                    if (bad > MAX_INVALID_PLS_LINES) {
                        /* For now, just break out of the loop. */
                        warn(_("%s: too many invalid lines (max allowed %d)\n"), quotearg(conv_filename(fullpath)), MAX_INVALID_PLS_LINES);
                        break;
                    }
                }
            } else if (type == FILE_EXTM3U) {
                if (len >= 8 && strncmp(buf, "#EXTINF:", 8) == 0) {
                    /* No operation - we're not interested in EXTINF */
                } else if (len == 0 || !fix_playlist_path(buf)) {
                    warn(_("%s: invalid line %d\n"), quotearg(conv_filename(fullpath)), ln);
                    bad++;
                } else {
                    if (!add_playlist_child(entry, &children_size, buf, playlist_dir, indent_size+1, &inl_new))
                        bad++;
                }
                if (bad > MAX_INVALID_EXTM3U_FILES) {
                    /* For now, just break out of the loop. */
                    warn(_("%s: too many invalid files (max allowed %d)\n"), quotearg(conv_filename(fullpath)), MAX_INVALID_EXTM3U_FILES);
                    break;
                }
            } else if (type == FILE_M3U) {
                if (len == 0 || !fix_playlist_path(buf)) {
                    warn(_("%s: invalid line %d\n"), quotearg(conv_filename(fullpath)), ln);
                    bad++;
                } else {
                    if (!add_playlist_child(entry, &children_size, buf, playlist_dir, indent_size+1, &inl_new))
                        bad++;
                }
                if (bad > MAX_INVALID_M3U_FILES) {
                    /* For now, just break out of the loop. */
                    warn(_("%s: too many invalid files (max allowed %d)\n"), quotearg(conv_filename(fullpath)), MAX_INVALID_M3U_FILES);
                    break;
                }
            }
        }
    }

    /* A read error here means that we may still have read some playlist
     * entries.
     */
    if (ferror(fh))
        warn(_("%s: cannot read from file: %s\n"), quotearg(conv_filename(fullpath)), errstr);

    fclose(fh); /* Ignore errors because we opened for reading */
    free(buf);
    free(playlist_dir);
    free(indent);
    return entry;
}
    
static Entry *
scan_entry(const char *fullpath, const char *name, int32_t parent, int indent_size, InodeList *inl)
{
    struct stat sb;
    char *indent;

    indent = xstrdupn(" ", indent_size*2);

    if (strncasecmp(fullpath, "http://", 7) == 0) {
        Entry *entry;
        EntryDetail *detail;

        say(4, _("%sAdding URL %s as %s\n"), indent, quote_n(0, conv_filename(fullpath)), quote_n(1, name));
        entry = make_entry(name, parent, false);
        detail = add_entry_detail(entry, DETAIL_URL);
        detail->data.url.url = xstrdup(fullpath);

	free(indent);
        return entry;
    }

    if (stat(fullpath, &sb) < 0) {
        warn(_("%s: cannot stat: %s\n"), quotearg(conv_filename(fullpath)), errstr);
	free(indent);
        return NULL;
    }

    if (S_ISDIR(sb.st_mode)) {
        Entry *entry;
	EntryDetail *detail;
	int32_t *children;
	uint32_t child_count;
        struct dirent **dirents;
        int dirent_count;
        int c;
        InodeList inl_new;

	say(4, _("%sScanning directory %s\n"), indent, quote(conv_filename(fullpath)));
	if (inode_in_list(sb.st_ino, inl)) {
	    warn(_("%s: recursive loop detected - ignoring\n"), quotearg(conv_filename(fullpath)));
	    free(indent);
	    return NULL;
	}

        dirent_count = scandir(fullpath, &dirents, NULL, alphasort);
        if (dirent_count < 0) {
            warn(_("%s: cannot scan directory: %s\n"), quotearg(conv_filename(fullpath)), errstr);
	    free(indent);
            return NULL;
        }

        inl_new.prev = inl;
        inl_new.node = sb.st_ino;
        entry = make_entry(name, parent, true);
	children = xmalloc(sizeof(int32_t) * dirent_count);

	child_count = 0;
        for (c = 0; c < dirent_count; c++) {
            if (strcmp(dirents[c]->d_name, ".") != 0 && strcmp(dirents[c]->d_name, "..") != 0) {
		Entry *child;
		char *child_path;
		char *child_name;

		child_path = concat_filenames(fullpath, dirents[c]->d_name);
		child_name = xstrdup(conv_filename(dirents[c]->d_name));
		child = scan_entry(child_path, child_name, entry->id, indent_size+1, &inl_new);
		free(child_name);
		if (child != NULL)
		    children[child_count++] = child->id;
		free(child_path);
	    }
        }

	detail = get_entry_detail(entry, DETAIL_CHILDREN);
	detail->data.children.count = child_count;
	detail->data.children.list = xmemdup(children, sizeof(int32_t) * child_count);
	free(children);

	free(indent);
        return entry;
    }

    if (S_ISREG(sb.st_mode)) {
	Entry *entry;
	EntryDetail *detail;
	FileType type;

        if (parent == -1) {
            warn(_("%s: root must be a directory\n"), quotearg(conv_filename(fullpath)));
	    free(indent);
            return NULL;
        }

        say(4, _("%sFound regular file %s\n"), indent, quote(name));
	type = check_file_content_type(fullpath);
	if (type == FILE_UNKNOWN) {
	    say(4, _("%s  Matched no type\n"), indent);
            /*say(4, _("%s: skipping file - unrecognized content type\n"), quotearg(fullpath));*/
        } else if (type == FILE_M3U) {
	    say(4, _("%s  Assuming type %s - %s\n"), indent, file_type_descs[type], file_type_mime_types[type]);
        } else {
	    say(4, _("%s  Matched type %s - %s\n"), indent, file_type_descs[type], file_type_mime_types[type]);
        }
	if (!string_in_csv(file_types, ',', file_type_names[type])) {
	    say(4, _("%s  Skipping (file type excluded)\n"), indent);
	    free(indent);
	    return NULL;
	}

        if (type == FILE_PLS || type == FILE_M3U || type == FILE_EXTM3U) {
            free(indent);
	    return scan_playlist_file(fullpath, name, parent, type, indent_size+1, inl, sb.st_ino);
	}

        /*say(4, _("%s  Adding as %s\n"), indent, quote(name));*/
        entry = make_entry(name, parent, false);

        detail = add_entry_detail(entry, DETAIL_FILE);
        detail->data.file.size = sb.st_size;
	detail->data.file.mode = sb.st_mode;
        detail->data.file.mtime = sb.st_mtime;
        detail->data.file.filename = xstrdup(fullpath);
	detail->data.file.mime_type = file_type_mime_types[type];
	detail->data.file.item_class = file_type_item_classes[type];
	/* XXX: dump size, mtime, ?? */

	if (file_type_dlna_pn[type] != NULL) {
	  detail->data.file.ext_info = xasprintf("%s%s%s", EXT_INFO_DEFAULT, EXT_INFO_PN, file_type_dlna_pn[type]);
        } else {
          detail->data.file.ext_info = xstrdup(EXT_INFO_DEFAULT);
        }

	if (tags_enabled && (type == FILE_MP3 || type == FILE_MP3_ID3)) {
	    detail = NULL;

#ifdef HAVE_TAGLIB
            if (detail == NULL) {
                TagLib_File *tl_file;
                TagLib_File_Type tl_type;

                switch (type) {
                case FILE_OGG:
                    tl_type = TagLib_File_OggVorbis;
                    break;
                default:
                    tl_type = TagLib_File_MPEG;
                    break;
                }

                tl_file = taglib_file_new_type(fullpath, tl_type); 
                if (tl_file != NULL) {
                    TagLib_Tag *tl_tag;
                    const TagLib_AudioProperties *tl_props;
		    char *str;
                    unsigned int track;

                    tl_tag = taglib_file_tag(tl_file);
                    detail = add_entry_detail(entry, DETAIL_TAG);
		    str = taglib_tag_title(tl_tag);
		    detail->data.tag.title = (*str == '\0' ? NULL : xstrdup(str));
		    str = taglib_tag_album(tl_tag);
                    detail->data.tag.album = (*str == '\0' ? NULL : xstrdup(str));
		    str = taglib_tag_artist(tl_tag);
                    detail->data.tag.artist = (*str == '\0' ? NULL : xstrdup(str));
		    str = taglib_tag_genre(tl_tag);
                    detail->data.tag.genre = (*str == '\0' ? NULL : xstrdup(str));
                    track = taglib_tag_track(tl_tag);
                    detail->data.tag.track_no = track > 0 ? track : -1;
                    tl_props = taglib_file_audioproperties(tl_file);
                    detail->data.tag.duration = taglib_audioproperties_length(tl_props);

                    /*taglib_tag_free_strings();*/
                    taglib_file_free(tl_file);
                }
            }
#endif
#ifdef HAVE_ID3LIB
            if (detail == NULL) {
                /*char *title;*/

                ID3Tag_Clear(id3);
                ID3Tag_Link(id3, fullpath);

                if (ID3Tag_NumFrames(id3) != 0) {
                    char *track;

                    detail = add_entry_detail(entry, DETAIL_TAG);
                    /*title = get_id3_string(fullpath, id3, ID3FID_TITLE);
                    if (title != NULL && strcmp(title, "") != 0) {
                        free(entry->name);
                        entry->name = title;
                    } else {
                        free(title);
                    }*/
                    detail->data.tag.title = get_id3_string(fullpath, id3, ID3FID_TITLE);
                    detail->data.tag.album = get_id3_string(fullpath, id3, ID3FID_ALBUM);
                    detail->data.tag.artist = get_id3_string(fullpath, id3, ID3FID_LEADARTIST);
                    detail->data.tag.genre = get_id3_string(fullpath, id3, ID3FID_CONTENTTYPE);
                    track = get_id3_string(fullpath, id3, ID3FID_TRACKNUM);
                    if (track == NULL || !parse_int32(track, &detail->data.tag.track_no))
                        detail->data.tag.track_no = -1;

                    /* get_id3_string(fullpath, id3, ID3FID_TIME); */
                    detail->data.tag.duration = -1;
                }
            }
#endif
	    if (detail != NULL) {
		if (detail->data.tag.title != NULL)
		    say(4, "%s  %s: %s\n", indent, _("Title"), quotearg(detail->data.tag.title));
		if (detail->data.tag.artist != NULL)
		    say(4, "%s  %s: %s\n", indent, _("Artist"), quotearg(detail->data.tag.artist));
		if (detail->data.tag.album != NULL)
		    say(4, "%s  %s: %s\n", indent, _("Album"), quotearg(detail->data.tag.album));
		if (detail->data.tag.genre != NULL)
		    say(4, "%s  %s: %s\n", indent, _("Genre"), quotearg(detail->data.tag.genre));
		if (detail->data.tag.track_no != -1)
		    say(4, "%s  %s: %" PRId32 "\n", indent, _("Track number"), detail->data.tag.track_no);
                if (detail->data.tag.duration != -1)
                    say(4, "%s  %s: %u:%02u:%02u.%02u\n", indent, _("Duration"), SPLIT_DURATION(detail->data.tag.duration));
	    }
        }

	free(indent);
        return entry;
    }

    if (S_ISBLK(sb.st_mode) || S_ISCHR(sb.st_mode) || S_ISFIFO(sb.st_mode)) {
	Entry *entry;
	EntryDetail *detail;

        if (parent == -1) {
            warn(_("%s: root must be a directory\n"), quotearg(conv_filename(fullpath)));
	    free(indent);
            return NULL;
        }
        say(4, _("%sFound device/FIFO file %s\n"), indent, quote(name));

        entry = make_entry(name, parent, false);
        detail = add_entry_detail(entry, DETAIL_FILE);
        detail->data.file.size = sb.st_size;
	detail->data.file.mode = sb.st_mode;
        detail->data.file.mtime = sb.st_mtime;
        detail->data.file.filename = xstrdup(fullpath);
	detail->data.file.mime_type = file_type_mime_types[FILE_MP3]; /* FIXME: unknown type. */
	detail->data.file.item_class = file_type_item_classes[FILE_MP3];

	free(indent);
	return entry;
    }

    warn(_("%s: skipping file - unsupported file type\n"), quotearg(conv_filename(fullpath)));
    free(indent);
    return NULL;
}

bool
init_metadata(void)
{
#ifndef MAGIC_MIME_TYPE /* old version */
#define MAGIC_MIME_TYPE MAGIC_MIME
#endif
    magic_cookie = magic_open(MAGIC_SYMLINK|MAGIC_MIME_TYPE|MAGIC_ERROR);
    if (magic_cookie == NULL) {
        warn(_("cannot initialize magic library\n"));
        return false;
    }

    say(4, _("Loading default magic database\n"));
    if (magic_load(magic_cookie, NULL) != 0) {
        warn(_("cannot load magic database - %s\n"), magic_error(magic_cookie));
        magic_close(magic_cookie);
        return false;
    }

#ifdef HAVE_ID3LIB
    if (tags_enabled) {
        iso8859_1_to_utf8 = iconv_open("UTF-8", "ISO8859-1");
        if (iso8859_1_to_utf8 == (iconv_t) -1) {
             warn(_("cannot create character set convertor from %s to %s\n"), "ISO8859-1", "UTF-8");
             magic_close(magic_cookie);
             return false;
        }
        /* ID3 <= v2.3 uses UTF-16, because the BOM is included. Or to be more correct,
         * it uses UCS-2, but since that is a subset of UTF-16 and ID3 >= v2.4 uses UTF-16
         * we use that in this convertor instead.
         */
        utf16_to_utf8 = iconv_open("UTF-8", "UTF-16");
        if (utf16_to_utf8 == (iconv_t) -1) {
             warn(_("cannot create character set convertor from %s to %s\n"), "UTF-16", "UTF-8");
             iconv_close(iso8859_1_to_utf8); /* ignore errors */
             magic_close(magic_cookie);
             return false;
        }
	id3 = ID3Tag_New();
    }
#endif

    ithread_mutex_init(&metadata_mutex, NULL);

    entries_size = DEFAULT_ENTRIES_SIZE;
    entries = xmalloc(sizeof(Entry *) * entries_size);

    return true;
}

bool
scan_entries(char **pathv, int pathc, int indent_size)
{
    if (pathc > 1) {
	int32_t *children;
	EntryDetail *detail;
	uint32_t c;
	uint32_t child_count = 0;

	children = xmalloc(sizeof(int32_t) * pathc);
	for (c = 0; c < pathc; c++) {
	    Entry *entry;
	    char *name;
	    uint32_t d;

	    name = base_name(pathv[c]);
	    if (*name == '\0' || *name == '/') {
	        name = xstrdup(conv_filename(pathv[c]));
            } else {
                char *tmp;
	        for (d = strlen(name)-1; name[d] == '/'; d--);
	        tmp = xstrndup(name, d+1);
	        name = xstrdup(conv_filename(tmp));
	        free(tmp);
            }

	    entry = scan_entry(pathv[c], name, -1, indent_size, NULL);
	    if (entry != NULL)
	        children[child_count++] = entry->id;
	    free(name);
	}
	if (child_count != 0) {
	    root_entry = make_entry(ROOT_ENTRY_NAME, -1, true);
	    detail = get_entry_detail(root_entry, DETAIL_CHILDREN);
	    detail->data.children.count = child_count;
	    detail->data.children.list = children;
        } else {
            root_entry = NULL;
            free(children);
        }
    } else {
	root_entry = scan_entry(pathv[0], ROOT_ENTRY_NAME, -1, indent_size, NULL);
    }


    return root_entry != NULL;
}

void
clear_entries(void)
{
    uint32_t c;

    for (c = 0; c < entry_count; c++) {
	EntryDetail *d = entries[c]->details;

	while (d != NULL) {
	    EntryDetail *next;

	    if (d->type == DETAIL_CHILDREN) {
		free(d->data.children.list);
	    } else if (d->type == DETAIL_FILE) {
	        free(d->data.file.filename);
                free(d->data.file.ext_info);
	    } else if (d->type == DETAIL_TAG) {
		free(d->data.tag.title);
		free(d->data.tag.artist);
		free(d->data.tag.album);
		free(d->data.tag.genre);
	    }

	    next = d->next;
	    free(d);
	    d = next;
	}

	free(entries[c]->name);
        free(entries[c]);
    }

    entry_count = 0;
}

void
finish_metadata(void)
{
    clear_entries();
#ifdef HAVE_ID3LIB
    if (tags_enabled) {
	ID3Tag_Delete(id3);
	iconv_close(iso8859_1_to_utf8); /* ignore errors (only EINVAL) */
	iconv_close(utf16_to_utf8); /* ignore errors (only EINVAL) */
    }
#endif
    free(entries);
    entries_size = 0;

    ithread_mutex_destroy(&metadata_mutex);
}

void
lock_metadata(void)
{
    ithread_mutex_lock(&metadata_mutex);
}

void
unlock_metadata(void)
{
    ithread_mutex_unlock(&metadata_mutex);
}
