/*  Audacious
 *  Copyright (C) 2005-2007  Audacious team.
 *
 *  BMP (C) GPL 2003 $top_src_dir/AUTHORS
 *
 *  based on:
 *
 *  XMMS - Cross-platform multimedia player
 *  Copyright (C) 1998-2003  Peter Alm, Mikael Alm, Olle Hallnas,
 *                           Thomas Nilsson and 4Front Technologies
 *  Copyright (C) 1999-2003  Haavard Kvaalen
 *
 *  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; under version 3 of the License.
 *
 *  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, see <http://www.gnu.org/licenses>.
 *
 *  The Audacious team does not consider modular code linking to
 *  Audacious or using our public API to be a derived work.
 */

/* #define AUD_DEBUG 1 */

#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif

#include "playlist.h"

#include <glib.h>
#include <glib/gprintf.h>
#include <mowgli.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/errno.h>
#include <time.h>
#include <unistd.h>

#if defined(USE_REGEX_ONIGURUMA)
#  include <onigposix.h>
#elif defined(USE_REGEX_PCRE)
#  include <pcreposix.h>
#else
#  include <regex.h>
#endif

#include "configdb.h"
#include "hook.h"
#include "input.h"
#include "playback.h"
#include "playlist_container.h"
#include "playlist_evmessages.h"
#include "pluginenum.h"
#include "strings.h"
#include "ui_main.h"
#include "ui_playlist.h"
#include "util.h"
#include "vfs.h"

#include "debug.h"

typedef gint (*PlaylistCompareFunc) (PlaylistEntry * a, PlaylistEntry * b);
typedef void (*PlaylistSaveFunc) (FILE * file);

/* If we manually change the song, p_p_b_j will show us where to go back to */
static PlaylistEntry *playlist_position_before_jump = NULL;

static GList *playlists = NULL;
static GList *playlists_iter;


/* If this is set to TRUE, we do not probe upon playlist add.
 *
 * Under Audacious 0.1.x, this was not a big deal because we used 
 * file extension introspection instead of looking for file format magic
 * strings.
 *
 * Because we use file magic strings, we have to fstat a file being added
 * to a playlist up to 1 * <number of input plugins installed> times.
 *
 * This can get really slow now that we're looking for files to add to a
 * playlist. (Up to 5 minutes for 5000 songs, etcetera.)
 *
 * So, we obviously don't want to probe while opening a large playlist 
 * up. Hince the boolean below.
 *
 * January 7, 2006, William Pitcock <nenolod@nenolod.net>
 */

static gboolean playlist_get_info_scan_active = FALSE;
static GStaticRWLock playlist_get_info_rwlock = G_STATIC_RW_LOCK_INIT;
static gboolean playlist_get_info_going = FALSE;
static GThread *playlist_get_info_thread;

extern GHashTable *ext_hash;

static gint path_compare(const gchar * a, const gchar * b);
static gint playlist_compare_path(PlaylistEntry * a, PlaylistEntry * b);
static gint playlist_compare_filename(PlaylistEntry * a, PlaylistEntry * b);
static gint playlist_compare_title(PlaylistEntry * a, PlaylistEntry * b);
static gint playlist_compare_artist(PlaylistEntry * a, PlaylistEntry * b);
static time_t playlist_get_mtime(const gchar *filename);
static gint playlist_compare_date(PlaylistEntry * a, PlaylistEntry * b);
static gint playlist_compare_track(PlaylistEntry * a, PlaylistEntry * b);
static gint playlist_compare_playlist(PlaylistEntry * a, PlaylistEntry * b);

static gint playlist_dupscmp_path(PlaylistEntry * a, PlaylistEntry * b);
static gint playlist_dupscmp_filename(PlaylistEntry * a, PlaylistEntry * b);
static gint playlist_dupscmp_title(PlaylistEntry * a, PlaylistEntry * b);

static PlaylistCompareFunc playlist_compare_func_table[] = {
    playlist_compare_path,
    playlist_compare_filename,
    playlist_compare_title,
    playlist_compare_artist,
    playlist_compare_date,
    playlist_compare_track,
    playlist_compare_playlist
};

static guint playlist_load_ins(Playlist * playlist, const gchar * filename, gint pos);

static void playlist_generate_shuffle_list(Playlist *);
static void playlist_generate_shuffle_list_nolock(Playlist *);

static void playlist_recalc_total_time_nolock(Playlist *);
static void playlist_recalc_total_time(Playlist *);
static gboolean playlist_entry_get_info(PlaylistEntry * entry);


#define EXT_TRUE    1
#define EXT_FALSE   0
#define EXT_HAVE_SUBTUNE    2
#define EXT_CUSTOM  3

static gint filter_by_extension(const gchar *filename);
static gboolean is_http(const gchar *filename);
static gboolean do_precheck(Playlist *playlist, const gchar *uri, ProbeResult **pr);

static mowgli_heap_t *playlist_entry_heap = NULL;

/* *********************** playlist entry code ********************** */

PlaylistEntry *
playlist_entry_new(const gchar * filename,
                   const gchar * title,
                   const gint length,
                   InputPlugin * dec)
{
    PlaylistEntry *entry;

    entry = mowgli_heap_alloc(playlist_entry_heap);
    entry->filename = g_strdup(filename);
    entry->title = str_to_utf8(title);
    entry->length = length;
    entry->selected = FALSE;
    entry->decoder = dec;

    /* only do this if we have a decoder, otherwise it just takes too long */
    if (entry->decoder)
        playlist_entry_get_info(entry);

    return entry;
}

void
playlist_entry_free(PlaylistEntry * entry)
{
    if (!entry)
        return;

    if (entry->tuple != NULL) {
        mowgli_object_unref(entry->tuple);
        entry->tuple = NULL;
    }

    g_free(entry->filename);
    g_free(entry->title);
    mowgli_heap_free(playlist_entry_heap, entry);
}

static gboolean
playlist_entry_get_info(PlaylistEntry * entry)
{
    Tuple *tuple = NULL;
    ProbeResult *pr = NULL;
    time_t modtime;
    const gchar *formatter;

    g_return_val_if_fail(entry != NULL, FALSE);

    if (entry->tuple == NULL || tuple_get_int(entry->tuple, FIELD_MTIME, NULL) > 0 ||
        tuple_get_int(entry->tuple, FIELD_MTIME, NULL) == -1)
        modtime = playlist_get_mtime(entry->filename);
    else
        modtime = 0;  /* URI -nenolod */

    if (str_has_prefix_nocase(entry->filename, "http:") &&
        g_thread_self() != playlist_get_info_thread)
        return FALSE;

    if (entry->decoder == NULL) {
        pr = input_check_file(entry->filename, FALSE);
        if (pr != NULL)
            entry->decoder = pr->ip;
    }

    /* renew tuple if file mtime is newer than tuple mtime. */
    if (entry->tuple){
        if (tuple_get_int(entry->tuple, FIELD_MTIME, NULL) == modtime) {
            if (pr != NULL) g_free(pr);

            if (entry->title_is_valid == FALSE) { /* update title even tuple is present and up to date --asphyx */
                AUDDBG("updating title from actual tuple\n");
                formatter = tuple_get_string(entry->tuple, FIELD_FORMATTER, NULL);
                if (entry->title != NULL) g_free(entry->title);
                entry->title = tuple_formatter_make_title_string(entry->tuple, formatter ?
                                                                 formatter : get_gentitle_format());
                entry->title_is_valid = TRUE;
                AUDDBG("new title: \"%s\"\n", entry->title);
            }

            return TRUE;
        } else {
            mowgli_object_unref(entry->tuple);
            entry->tuple = NULL;
        }
    }

    if (pr != NULL && pr->tuple != NULL)
        tuple = pr->tuple;
    else if (entry->decoder != NULL && entry->decoder->get_song_tuple != NULL)
    {
        plugin_set_current((Plugin *)(entry->decoder));
        tuple = entry->decoder->get_song_tuple(entry->filename);
    }

    if (tuple == NULL) {
        if (pr != NULL) g_free(pr);
        return FALSE;
    }

    /* attach mtime */
    tuple_associate_int(tuple, FIELD_MTIME, NULL, modtime);

    /* entry is still around */
    formatter = tuple_get_string(tuple, FIELD_FORMATTER, NULL);
    entry->title = tuple_formatter_make_title_string(tuple, formatter ?
                                                     formatter : get_gentitle_format());
    entry->title_is_valid = TRUE;
    entry->length = tuple_get_int(tuple, FIELD_LENGTH, NULL);
    entry->tuple = tuple;

    if (pr != NULL) g_free(pr);

    return TRUE;
}

/* *********************** playlist selector code ************************* */

void
playlist_init(void)
{
    Playlist *initial_pl;

    /* create a heap with 1024 playlist entry nodes preallocated. --nenolod */
    playlist_entry_heap = mowgli_heap_create(sizeof(PlaylistEntry), 1024,
                                             BH_NOW);

    /* FIXME: is this really necessary? REQUIRE_STATIC_LOCK(playlists); */

    initial_pl = playlist_new();

    playlist_add_playlist(initial_pl);
}

void
playlist_add_playlist(Playlist *playlist)
{
    playlists = g_list_append(playlists, playlist);

    if (playlists_iter == NULL)
        playlists_iter = playlists;

    hook_call("playlist update", NULL);
}

void
playlist_remove_playlist(Playlist *playlist)
{
    gboolean active;
    active = (playlist && playlist == playlist_get_active());
    /* users suppose playback will be stopped on removing playlist */
    if (active && playback_get_playing()) {
        ip_data.stop = TRUE;
        playback_stop();
        ip_data.stop = FALSE;
        hook_call("playlist end reached", NULL);
    }

    /* trying to free the last playlist simply clears and resets it */
    if (g_list_length(playlists) < 2) {
        playlist_clear(playlist);
        playlist_set_current_name(playlist, NULL);
        playlist_filename_set(playlist, NULL);
        return;
    }

    if (active) playlist_select_next();

    /* upon removal, a playlist should be cleared and freed */
    playlists = g_list_remove(playlists, playlist);
    playlist_clear(playlist);
    playlist_free(playlist);

    if (playlists_iter == NULL)
        playlists_iter = playlists;

    hook_call("playlist update", NULL);
}

GList *
playlist_get_playlists(void)
{
    return playlists;
}

void
playlist_select_next(void)
{
    if (playlists_iter == NULL)
        playlists_iter = playlists;

    playlists_iter = g_list_next(playlists_iter);

    if (playlists_iter == NULL)
        playlists_iter = playlists;

    hook_call("playlist update", playlist_get_active());
}

void
playlist_select_prev(void)
{
    if (playlists_iter == NULL)
        playlists_iter = playlists;

    playlists_iter = g_list_previous(playlists_iter);

    if (playlists_iter == NULL)
        playlists_iter = playlists;

    hook_call("playlist update", playlist_get_active());
}

void
playlist_select_playlist(Playlist *playlist)
{
    playlists_iter = g_list_find(playlists, playlist);

    if (playlists_iter == NULL)
        playlists_iter = playlists;

    hook_call("playlist update", playlist);
}

/* *********************** playlist code ********************** */

const gchar *
playlist_get_current_name(Playlist *playlist)
{
    return playlist->title;
}

/* This function now sets the playlist title, not the playlist filename.
 * See playlist_filename_set */
gboolean
playlist_set_current_name(Playlist *playlist, const gchar * title)
{
    gchar *oldtitle = playlist->title;

    if (!title) {
        playlist->title = NULL;
        g_free(oldtitle);
        hook_call("playlist update", NULL);
        return FALSE;
    }

    playlist->title = str_to_utf8(title);
    g_free(oldtitle);
    hook_call("playlist update", NULL);
    return TRUE;
}

/* Setting the filename allows the original playlist to be modified later */
gboolean
playlist_filename_set(Playlist *playlist, const gchar * filename)
{
    gchar *old;
    old = playlist->filename;

    if (!filename) {
        playlist->filename = NULL;
        g_free(old);
        return FALSE;
    }

    playlist->filename = filename_to_utf8(filename);
    g_free(old);
    return TRUE;
}

gchar *
playlist_filename_get(Playlist *playlist)
{
    if (!playlist->filename) return NULL;
    return g_filename_from_utf8(playlist->filename, -1, NULL, NULL, NULL);
}

static GList *
find_playlist_position_list(Playlist *playlist)
{
    REQUIRE_LOCK(playlist->mutex);

    if (!playlist->position) {
        if (cfg.shuffle)
            return playlist->shuffle;
        else
            return playlist->entries;
    }

    if (cfg.shuffle)
        return g_list_find(playlist->shuffle, playlist->position);
    else
        return g_list_find(playlist->entries, playlist->position);
}

static void
play_queued(Playlist *playlist)
{
    GList *tmp = playlist->queue;

    REQUIRE_LOCK(playlist->mutex);

    playlist->position = playlist->queue->data;
    playlist->queue = g_list_remove_link(playlist->queue, playlist->queue);
    g_list_free_1(tmp);
}

void
playlist_clear_only(Playlist *playlist)
{
    PLAYLIST_LOCK(playlist);

    g_list_foreach(playlist->entries, (GFunc) playlist_entry_free, NULL);
    g_list_free(playlist->entries);
    playlist->position = NULL;
    playlist->entries = NULL;
    playlist->tail = NULL;
    playlist->attribute = PLAYLIST_PLAIN;
    playlist->serial = 0;

    PLAYLIST_UNLOCK(playlist);
}

void
playlist_clear(Playlist *playlist)
{
    if (!playlist)
        return;

    playlist_clear_only(playlist);
    playlist_generate_shuffle_list(playlist);
    hook_call("playlist update", playlist);
    playlist_recalc_total_time(playlist);
    PLAYLIST_INCR_SERIAL(playlist);
}

static void
playlist_delete_node(Playlist * playlist, GList * node, gboolean * set_info_text,
                     gboolean * restart_playing)
{
    PlaylistEntry *entry;
    GList *playing_song = NULL;

    REQUIRE_LOCK(playlist->mutex);

    /* We call g_list_find manually here because we don't want an item
     * in the shuffle_list */

    if (playlist->position)
        playing_song = g_list_find(playlist->entries, playlist->position);

    entry = PLAYLIST_ENTRY(node->data);

    if (playing_song == node) {
        *set_info_text = TRUE;

        if (playback_get_playing()) {
            PLAYLIST_UNLOCK(playlist);
            ip_data.stop = TRUE;
            playback_stop();
            ip_data.stop = FALSE;
            PLAYLIST_LOCK(playlist);
            *restart_playing = TRUE;
        }

        playing_song = find_playlist_position_list(playlist);

        if (g_list_next(playing_song))
            playlist->position = g_list_next(playing_song)->data;
        else if (g_list_previous(playing_song))
            playlist->position = g_list_previous(playing_song)->data;
        else
            playlist->position = NULL;

        /* Make sure the entry did not disappear under us */
        if (g_list_index(playlist->entries, entry) == -1)
            return;

    }
    else if (g_list_position(playlist->entries, playing_song) >
             g_list_position(playlist->entries, node)) {
        *set_info_text = TRUE;
    }

    playlist->shuffle = g_list_remove(playlist->shuffle, entry);
    playlist->queue = g_list_remove(playlist->queue, entry);
    playlist->entries = g_list_remove_link(playlist->entries, node);
    playlist->tail = g_list_last(playlist->entries);
    playlist_entry_free(entry);
    g_list_free_1(node);

    playlist_recalc_total_time_nolock(playlist);
    PLAYLIST_INCR_SERIAL(playlist);
}

void
playlist_delete_index(Playlist *playlist, guint pos)
{
    gboolean restart_playing = FALSE, set_info_text = FALSE;
    GList *node;

    if (!playlist)
        return;

    PLAYLIST_LOCK(playlist);

    node = g_list_nth(playlist->entries, pos);

    if (!node) {
        PLAYLIST_UNLOCK(playlist);
        return;
    }

    playlist_delete_node(playlist, node, &set_info_text, &restart_playing);

    PLAYLIST_UNLOCK(playlist);

    playlist_recalc_total_time(playlist);
    PLAYLIST_INCR_SERIAL(playlist);

    hook_call("playlist update", playlist);
    if (restart_playing) {
        if (playlist->position)
            playback_initiate();
        else
            hook_call("playlist end reached", NULL);
    }
}

void
playlist_delete(Playlist * playlist, gboolean crop)
{
    gboolean restart_playing = FALSE, set_info_text = FALSE;
    GList *node, *next_node;
    PlaylistEntry *entry;

    g_return_if_fail(playlist != NULL);

    PLAYLIST_LOCK(playlist);

    node = playlist->entries;

    while (node) {
        entry = PLAYLIST_ENTRY(node->data);

        next_node = g_list_next(node);

        if ((entry->selected && !crop) || (!entry->selected && crop)) {
            playlist_delete_node(playlist, node, &set_info_text, &restart_playing);
        }

        node = next_node;
    }

    PLAYLIST_UNLOCK(playlist);

    playlist_recalc_total_time(playlist);
    PLAYLIST_INCR_SERIAL(playlist);

    if (restart_playing) {
        if (playlist->position)
            playback_initiate();
        else
            hook_call("playlist end reached", NULL);
    }

    hook_call("playlist update", playlist);
}

static void
__playlist_ins_file(Playlist * playlist,
                    const gchar * filename,
                    gint pos,
                    Tuple *tuple,
                    const gchar *title, // may NULL
                    gint len,
                    InputPlugin * dec)
{
    PlaylistEntry *entry;
    Tuple *parent_tuple = NULL;
    gint nsubtunes = 0, subtune = 0;
    gboolean add_flag = TRUE;

    g_return_if_fail(playlist != NULL);
    g_return_if_fail(filename != NULL);

    if (tuple != NULL) {
        nsubtunes = tuple->nsubtunes;
        if (nsubtunes > 0) {
            parent_tuple = tuple;
            subtune = 1;
        }
    }

    for (; add_flag && subtune <= nsubtunes; subtune++) {
        gchar *filename_entry;

        if (nsubtunes > 0) {
            filename_entry = g_strdup_printf("%s?%d", filename,
                                             parent_tuple->subtunes ? parent_tuple->subtunes[subtune - 1] : subtune);

            /* We're dealing with subtune, let's ask again tuple information
             * to plugin, by passing the ?subtune suffix; this way we get
             * specific subtune information in the tuple, if available.
             */
            plugin_set_current((Plugin *)dec);
            tuple = dec->get_song_tuple(filename_entry);
        } else
            filename_entry = g_strdup(filename);

        if (tuple) {
            entry = playlist_entry_new(filename_entry,
                                       tuple_get_string(tuple, FIELD_TITLE, NULL),
                                       tuple_get_int(tuple, FIELD_LENGTH, NULL), dec);
        }
        else {
            entry = playlist_entry_new(filename_entry, title, len, dec);
        }

        g_free(filename_entry);


        PLAYLIST_LOCK(playlist);

        if (!playlist->tail)
            playlist->tail = g_list_last(playlist->entries);

        if (pos == -1) { // the common case
            GList *element;
            element = g_list_alloc();
            element->data = entry;
            element->prev = playlist->tail; // NULL is allowed here.
            element->next = NULL;

            if(!playlist->entries) { // this is the first element
                playlist->entries = element;
                playlist->tail = element;
            } else { // the rests
                if (playlist->tail != NULL) {
                    playlist->tail->next = element;
                    playlist->tail = element;
                } else
                    add_flag = FALSE;
            }
        } else
            playlist->entries = g_list_insert(playlist->entries, entry, pos++);

        if (tuple != NULL) {
            const gchar *formatter = tuple_get_string(tuple, FIELD_FORMATTER, NULL);
            g_free(entry->title);
            entry->title = tuple_formatter_make_title_string(tuple,
                                                             formatter ? formatter : get_gentitle_format());
            entry->title_is_valid = TRUE;
            entry->length = tuple_get_int(tuple, FIELD_LENGTH, NULL);
            entry->tuple = tuple;
        }

        PLAYLIST_UNLOCK(playlist);
    }

    if (parent_tuple)
        tuple_free(parent_tuple);

    if (!tuple || (tuple && tuple_get_int(tuple, FIELD_MTIME, NULL) == -1)) {
        // kick the scanner thread when tuple == NULL or mtime = -1 (uninitialized)
        g_mutex_lock(mutex_scan);
        playlist_get_info_scan_active = TRUE;
        g_mutex_unlock(mutex_scan);
        g_cond_signal(cond_scan);
    }
    PLAYLIST_INCR_SERIAL(playlist);
}

gboolean
playlist_ins(Playlist * playlist, const gchar * filename, gint pos)
{
    gchar buf[64], *p;
    gint r;
    VFSFile *file;
    ProbeResult *pr = NULL;
    InputPlugin *dec = NULL;
    Tuple *tuple = NULL;
    gboolean http_flag = is_http(filename);

    g_return_val_if_fail(playlist != NULL, FALSE);
    g_return_val_if_fail(filename != NULL, FALSE);

    PLAYLIST_INCR_SERIAL(playlist);

    /* load playlist */
    if (is_playlist_name(filename)) {
        playlist->loading_playlist = TRUE;
        playlist_load_ins(playlist, filename, pos);
        playlist->loading_playlist = FALSE;
        return TRUE;
    }

    if (do_precheck(playlist, filename, &pr)) {
        if (pr) {
            dec = pr->ip;
            tuple = pr->tuple;
        }
        /* add filename to playlist */
        if (cfg.playlist_detect == TRUE ||
            playlist->loading_playlist == TRUE ||
            (playlist->loading_playlist == FALSE && dec != NULL) ||
            (playlist->loading_playlist == FALSE && !is_playlist_name(filename)
             && http_flag))
        {
            __playlist_ins_file(playlist, filename, pos, tuple, NULL, -1, dec);

            g_free(pr);
            playlist_generate_shuffle_list(playlist);
            hook_call("playlist update", playlist);
            return TRUE;
        }
    }

    /* Some files (typically produced by some cgi-scripts) don't have
     * the correct extension.  Try to recognize these files by looking
     * at their content.  We only check for http entries since it does
     * not make sense to have file entries in a playlist fetched from
     * the net. */

    /* Some strange people put fifo's with the .mp3 extension, so we
     * need to make sure it's a real file (otherwise fread() may block
     * and stall the entire program) */

    /* FIXME: bah, FIFOs actually pass this regular file test */
    if (!vfs_file_test(filename, G_FILE_TEST_IS_REGULAR))
        return FALSE;

    if ((file = vfs_fopen(filename, "rb")) == NULL)
        return FALSE;

    r = vfs_fread(buf, 1, sizeof(buf), file);
    vfs_fclose(file);

    for (p = buf; r-- > 0 && (*p == '\r' || *p == '\n'); p++);

    if (r > 5 && str_has_prefix_nocase(p, "http:")) {
        playlist_load_ins(playlist, filename, pos);
        return TRUE;
    }

    if (r > 6 && str_has_prefix_nocase(p, "https:")) {
        playlist_load_ins(playlist, filename, pos);
        return TRUE;
    }

    return FALSE;
}

/* FIXME: The next few functions are specific to Unix
 * filesystems. Either abstract it away, or don't even bother checking
 * at such low level */

typedef struct {
    dev_t dev;
    ino_t ino;
} DeviceInode;

static DeviceInode *
devino_new(dev_t device,
           ino_t inode)
{
    DeviceInode *devino = g_new0(DeviceInode, 1);

    if (devino) {
        devino->dev = device;
        devino->ino = inode;
    }

    return devino;
}

static guint
devino_hash(gconstpointer key)
{
    const DeviceInode *d = key;
    return d->ino;
}

static gint
devino_compare(gconstpointer a,
               gconstpointer b)
{
    const DeviceInode *da = a, *db = b;
    return (da->dev == db->dev && da->ino == db->ino);
}

static gboolean
devino_destroy(gpointer key, 
               gpointer value,
               gpointer data)
{
    g_free(key);
    return TRUE;
}

static gboolean
file_is_hidden(const gchar * filename)
{
    g_return_val_if_fail(filename != NULL, FALSE);
    return (g_basename(filename)[0] == '.');
}

static GList *
playlist_dir_find_files(const gchar * path,
                        gboolean background,
                        GHashTable * htab)
{
    GDir *dir;
    GList *list = NULL, *ilist;
    const gchar *dir_entry;
    ProbeResult *pr = NULL;

    struct stat statbuf;
    DeviceInode *devino;

    if (!path)
        return NULL;

    if (!vfs_file_test(path, G_FILE_TEST_IS_DIR))
        return NULL;

    stat(path, &statbuf);
    devino = devino_new(statbuf.st_dev, statbuf.st_ino);

    if (g_hash_table_lookup(htab, devino)) {
        g_free(devino);
        return NULL;
    }

    g_hash_table_insert(htab, devino, GINT_TO_POINTER(1));

    /* XXX: what the hell is this for? --nenolod */
    if ((ilist = input_scan_dir(path))) {
        GList *node;
        for (node = ilist; node; node = g_list_next(node)) {
            gchar *name = g_build_filename(path, node->data, NULL);
            list = g_list_prepend(list, name);
            g_free(node->data);
        }
        g_list_free(ilist);
        return list;
    }

    /* g_dir_open does not handle URI, so path should come here not-urified. --giacomo */
    if (!(dir = g_dir_open(path, 0, NULL)))
        return NULL;

    while ((dir_entry = g_dir_read_name(dir))) {
        gchar *filename, *tmp;
        gint ext_flag;
        gboolean http_flag;

        if (file_is_hidden(dir_entry))
            continue;

        tmp = g_build_filename(path, dir_entry, NULL);
        filename = g_filename_to_uri(tmp, NULL, NULL);
        g_free(tmp);

        ext_flag = filter_by_extension(filename);
        http_flag = is_http(filename);

        if (vfs_file_test(filename, G_FILE_TEST_IS_DIR)) { /* directory */
            GList *sub;
            gchar *dirfilename = g_filename_from_uri(filename, NULL, NULL);
            sub = playlist_dir_find_files(dirfilename, background, htab);
            g_free(dirfilename);
            g_free(filename);
            list = g_list_concat(list, sub);
        }
        else if (cfg.playlist_detect && ext_flag != EXT_HAVE_SUBTUNE && ext_flag != EXT_CUSTOM) { /* local file, no probing, no subtune */
            if(cfg.use_extension_probing) {
                if(ext_flag == EXT_TRUE)
                    list = g_list_prepend(list, filename);
                else // ext_flag == EXT_FALSE => extension isn't known
                    g_free(filename);
            }
            else
                list = g_list_prepend(list, filename);
        }
        else if ((pr = input_check_file(filename, TRUE)) != NULL) /* local file, probing or have subtune */
        {
            list = g_list_prepend(list, filename);

            g_free(pr);
            pr = NULL;
        }
        else
            g_free(filename);

        while (background && gtk_events_pending())
            gtk_main_iteration();
    }
    g_dir_close(dir);

    return list;
}

gboolean
playlist_add(Playlist * playlist, const gchar * filename)
{
    return playlist_ins(playlist, filename, -1);
}

guint 
playlist_add_dir(Playlist * playlist, const gchar * directory)
{
    return playlist_ins_dir(playlist, directory, -1, TRUE);
}

guint
playlist_add_url(Playlist * playlist, const gchar * url)
{
    guint entries;
    entries = playlist_ins_url(playlist, url, -1);
    return entries;
}

guint
playlist_ins_dir(Playlist * playlist, const gchar * path,
                 gint pos,
                 gboolean background)
{
    guint entries = 0;
    GList *list, *node;
    GHashTable *htab;
    gchar *path2 = g_filename_from_uri(path, NULL, NULL);

    if (path2 == NULL)
        path2 = g_strdup(path);

    htab = g_hash_table_new(devino_hash, devino_compare);

    list = playlist_dir_find_files(path2, background, htab);
    list = g_list_sort(list, (GCompareFunc) path_compare);

    g_hash_table_foreach_remove(htab, devino_destroy, NULL);

    for (node = list; node; node = g_list_next(node)) {
        playlist_ins(playlist, node->data, pos);
        g_free(node->data);
        entries++;
        if (pos >= 0)
            pos++;
    }

    g_list_free(list);
    g_free(path2);

    playlist_recalc_total_time(playlist);
    playlist_generate_shuffle_list(playlist);
    hook_call("playlist update", playlist);
    return entries;
}

guint
playlist_ins_url(Playlist * playlist, const gchar * string,
                 gint pos)
{
    gchar *tmp;
    gint entries = 0;
    gchar *decoded = NULL;

    g_return_val_if_fail(playlist != NULL, 0);
    g_return_val_if_fail(string != NULL, 0);

    while (*string) {
        GList *node;
        guint i = 0;
        tmp = strchr(string, '\n');
        if (tmp) {
            if (*(tmp - 1) == '\r')
                *(tmp - 1) = '\0';
            *tmp = '\0';
        }

        decoded = g_strdup(string);

        if (vfs_file_test(decoded, G_FILE_TEST_IS_DIR))
            i = playlist_ins_dir(playlist, decoded, pos, FALSE);
        else if (is_playlist_name(decoded))
            i = playlist_load_ins(playlist, decoded, pos);
        else if (playlist_ins(playlist, decoded, pos))
            i = 1;

        g_free(decoded);

        PLAYLIST_LOCK(playlist);
        node = g_list_nth(playlist->entries, pos);
        PLAYLIST_UNLOCK(playlist);

        entries += i;

        if (pos >= 0)
            pos += i;
        if (!tmp)
            break;

        string = tmp + 1;
    }

    playlist_recalc_total_time(playlist);
    PLAYLIST_INCR_SERIAL(playlist);
    playlist_generate_shuffle_list(playlist);
    hook_call("playlist update", playlist);

    return entries;
}

/* set info for current song. */
void
playlist_set_info(Playlist * playlist, const gchar * title,
                  gint length, gint rate, gint freq, gint nch)
{
    PlaylistEventInfoChange *msg;
    gchar *text;

    g_return_if_fail(playlist != NULL);

    if (length == -1) {
        // event_queue hates NULL --yaz
        event_queue("hide seekbar", (gpointer)0xdeadbeef);
    }

    if (playlist->position) {
        g_free(playlist->position->title);
        playlist->position->title = g_strdup(title);
        playlist->position->length = length;

        // overwrite tuple::title, mainly for streaming. it may incur side effects. --yaz
        if (playlist->position->tuple && tuple_get_int(playlist->position->tuple, FIELD_LENGTH, NULL) == -1){
            tuple_disassociate(playlist->position->tuple, FIELD_TITLE, NULL);
            tuple_associate_string(playlist->position->tuple, FIELD_TITLE, NULL, title);
        }
    }

    playlist_recalc_total_time(playlist);

    /* broadcast a PlaylistEventInfoChange message. */
    msg = g_new0(PlaylistEventInfoChange, 1);
    msg->bitrate = rate;
    msg->samplerate = freq;
    msg->channels = nch;

    event_queue("playlist info change", msg);

    text = playlist_get_info_text(playlist);
    event_queue("title change", text);

    if ( playlist->position )
        hook_call( "playlist set info" , playlist->position );
}

/* wrapper for playlist_set_info. this function is called by input plugins. */
void
playlist_set_info_old_abi(const gchar * title, gint length, gint rate,
                          gint freq, gint nch)
{
    Playlist *playlist = playlist_get_active();
    playlist_set_info(playlist, title, length, rate, freq, nch);
}

void
playlist_check_pos_current(Playlist *playlist)
{
    gint pos, row, bottom;

    if (!playlist)
        return;

    PLAYLIST_LOCK(playlist);
    if (!playlist->position) {
        PLAYLIST_UNLOCK(playlist);
        return;
    }

    pos = g_list_index(playlist->entries, playlist->position);

    if (playlistwin_item_visible(pos)) {
        PLAYLIST_UNLOCK(playlist);
        return;
    }

    bottom = MAX(0, playlist_get_length(playlist) - playlistwin_list_get_visible_count());
    row = CLAMP(pos - playlistwin_list_get_visible_count() / 2, 0, bottom);
    PLAYLIST_UNLOCK(playlist);
    playlistwin_set_toprow(row);
    g_cond_signal(cond_scan);
}

void
playlist_next(Playlist *playlist)
{
    GList *plist_pos_list;
    gboolean restart_playing = FALSE;

    if (!playlist_get_length(playlist))
        return;

    PLAYLIST_LOCK(playlist);

    if ((playlist_position_before_jump != NULL) && playlist->queue == NULL) {
        playlist->position = playlist_position_before_jump;
        playlist_position_before_jump = NULL;
    }

    plist_pos_list = find_playlist_position_list(playlist);

    if (!cfg.repeat && !g_list_next(plist_pos_list) && playlist->queue == NULL)
    {
        PLAYLIST_UNLOCK(playlist);
        return;
    }

    if (playback_get_playing()) {
        /* We need to stop before changing playlist_position */
        PLAYLIST_UNLOCK(playlist);
        ip_data.stop = TRUE;
        playback_stop();
        ip_data.stop = FALSE;
        PLAYLIST_LOCK(playlist);
        restart_playing = TRUE;
    }

    plist_pos_list = find_playlist_position_list(playlist);
    if (playlist->queue != NULL)
        play_queued(playlist);
    else if (g_list_next(plist_pos_list))
        playlist->position = g_list_next(plist_pos_list)->data;
    else if (cfg.repeat) {
        playlist->position = NULL;
        playlist_generate_shuffle_list_nolock(playlist);
        if (cfg.shuffle)
            playlist->position = playlist->shuffle->data;
        else
            playlist->position = playlist->entries->data;
    }

    PLAYLIST_UNLOCK(playlist);

    playlist_check_pos_current(playlist);

    if (restart_playing)
        playback_initiate();

    hook_call("playlist update", playlist);
}

void
playlist_prev(Playlist *playlist)
{
    GList *plist_pos_list;
    gboolean restart_playing = FALSE;

    if (!playlist_get_length(playlist))
        return;

    PLAYLIST_LOCK(playlist);

    if ((playlist_position_before_jump != NULL) && playlist->queue == NULL) {
        playlist->position = playlist_position_before_jump;
        playlist_position_before_jump = NULL;
    }

    plist_pos_list = find_playlist_position_list(playlist);

    if (!cfg.repeat && !g_list_previous(plist_pos_list)) {
        PLAYLIST_UNLOCK(playlist);
        return;
    }

    if (playback_get_playing()) {
        /* We need to stop before changing playlist_position */
        PLAYLIST_UNLOCK(playlist);
        ip_data.stop = TRUE;
        playback_stop();
        ip_data.stop = FALSE;
        PLAYLIST_LOCK(playlist);
        restart_playing = TRUE;
    }

    plist_pos_list = find_playlist_position_list(playlist);
    if (g_list_previous(plist_pos_list)) {
        playlist->position = g_list_previous(plist_pos_list)->data;
    }
    else if (cfg.repeat) {
        GList *node;
        playlist->position = NULL;
        playlist_generate_shuffle_list_nolock(playlist);
        if (cfg.shuffle)
            node = g_list_last(playlist->shuffle);
        else
            node = g_list_last(playlist->entries);
        if (node)
            playlist->position = node->data;
    }

    PLAYLIST_UNLOCK(playlist);

    playlist_check_pos_current(playlist);

    if (restart_playing)
        playback_initiate();
    else
        hook_call("playlist update", playlist);
}

void
playlist_queue(Playlist *playlist)
{
    GList *list = playlist_get_selected(playlist);
    GList *it = list;

    PLAYLIST_LOCK(playlist);

    if ((cfg.shuffle) && (playlist_position_before_jump == NULL)) {
        /* Shuffling and this is our first manual jump. */
        playlist_position_before_jump = playlist->position;
    }

    while (it) {
        GList *next = g_list_next(it);
        GList *tmp;

        /* XXX: WTF? --nenolod */
        it->data = g_list_nth_data(playlist->entries, GPOINTER_TO_INT(it->data));
        if ((tmp = g_list_find(playlist->queue, it->data))) {
            playlist->queue = g_list_remove_link(playlist->queue, tmp);
            g_list_free_1(tmp);
            list = g_list_remove_link(list, it);
            g_list_free_1(it);
        }

        it = next;
    }

    playlist->queue = g_list_concat(playlist->queue, list);

    PLAYLIST_UNLOCK(playlist);

    playlist_recalc_total_time(playlist);
    hook_call("playlist update", playlist);
}

void
playlist_queue_position(Playlist *playlist, guint pos)
{
    GList *tmp;
    PlaylistEntry *entry;

    PLAYLIST_LOCK(playlist);

    if ((cfg.shuffle) && (playlist_position_before_jump == NULL))
    {
        /* Shuffling and this is our first manual jump. */
        playlist_position_before_jump = playlist->position;
    }

    entry = g_list_nth_data(playlist->entries, pos);
    if ((tmp = g_list_find(playlist->queue, entry))) {
        playlist->queue = g_list_remove_link(playlist->queue, tmp);
        g_list_free_1(tmp);
    }
    else
        playlist->queue = g_list_append(playlist->queue, entry);
    PLAYLIST_UNLOCK(playlist);

    playlist_recalc_total_time(playlist);
    hook_call("playlist update", playlist);
}

gboolean
playlist_is_position_queued(Playlist *playlist, guint pos)
{
    PlaylistEntry *entry;
    GList *tmp = NULL;

    PLAYLIST_LOCK(playlist);
    entry = g_list_nth_data(playlist->entries, pos);
    tmp = g_list_find(playlist->queue, entry);
    PLAYLIST_UNLOCK(playlist);

    return tmp != NULL;
}

gint
playlist_get_queue_position_number(Playlist *playlist, guint pos)
{
    PlaylistEntry *entry;
    gint tmp;

    PLAYLIST_LOCK(playlist);
    entry = g_list_nth_data(playlist->entries, pos);
    tmp = g_list_index(playlist->queue, entry);
    PLAYLIST_UNLOCK(playlist);

    return tmp;
}

gint
playlist_get_queue_qposition_number(Playlist *playlist, guint pos)
{
    PlaylistEntry *entry;
    gint tmp;

    PLAYLIST_LOCK(playlist);
    entry = g_list_nth_data(playlist->queue, pos);
    tmp = g_list_index(playlist->entries, entry);
    PLAYLIST_UNLOCK(playlist);

    return tmp;
}

void
playlist_clear_queue(Playlist *playlist)
{
    PLAYLIST_LOCK(playlist);
    g_list_free(playlist->queue);
    playlist->queue = NULL;
    PLAYLIST_UNLOCK(playlist);

    playlist_recalc_total_time(playlist);
    hook_call("playlist update", playlist);
}

void
playlist_queue_remove(Playlist *playlist, guint pos)
{
    void *entry;

    PLAYLIST_LOCK(playlist);
    entry = g_list_nth_data(playlist->entries, pos);
    playlist->queue = g_list_remove(playlist->queue, entry);
    PLAYLIST_UNLOCK(playlist);

    hook_call("playlist update", playlist);
}

gint
playlist_get_queue_position(Playlist *playlist, PlaylistEntry * entry)
{
    return g_list_index(playlist->queue, entry);
}

void
playlist_set_position(Playlist *playlist, guint pos)
{
    GList *node;
    gboolean restart_playing = FALSE;

    if (!playlist)
        return;

    PLAYLIST_LOCK(playlist);

    node = g_list_nth(playlist->entries, pos);
    if (!node) {
        PLAYLIST_UNLOCK(playlist);
        return;
    }

    if (playback_get_playing()) {
        /* We need to stop before changing playlist_position */
        PLAYLIST_UNLOCK(playlist);
        ip_data.stop = TRUE;
        playback_stop();
        ip_data.stop = FALSE;
        PLAYLIST_LOCK(playlist);
        restart_playing = TRUE;
    }

    if ((cfg.shuffle) && (playlist_position_before_jump == NULL))
    {
        /* Shuffling and this is our first manual jump. */
        playlist_position_before_jump = playlist->position;
    }

    playlist->position = node->data;
    PLAYLIST_UNLOCK(playlist);
    playlist_check_pos_current(playlist);

    if (restart_playing)
        playback_initiate();
    else
        hook_call("playlist update", playlist);
}

void
playlist_eof_reached(Playlist *playlist)
{
    GList *plist_pos_list;

    if ((cfg.no_playlist_advance && !cfg.repeat) || cfg.stopaftersong)
        ip_data.stop = TRUE;
    playback_stop();
    if ((cfg.no_playlist_advance && !cfg.repeat) || cfg.stopaftersong)  
        ip_data.stop = FALSE;

    hook_call("playback end", playlist->position);

    PLAYLIST_LOCK(playlist);

    if ((playlist_position_before_jump != NULL) && playlist->queue == NULL)
    {
        playlist->position = playlist_position_before_jump;
        playlist_position_before_jump = NULL;
    }

    plist_pos_list = find_playlist_position_list(playlist);

    if (cfg.no_playlist_advance) {
        PLAYLIST_UNLOCK(playlist);
        if (cfg.repeat)
            playback_initiate();
        else
            hook_call("playlist end reached", NULL);
        return;
    }

    if (cfg.stopaftersong) {
        PLAYLIST_UNLOCK(playlist);
        hook_call("playlist end reached", NULL);
        mainwin_set_stopaftersong(FALSE);
        return;
    }

    if (playlist->queue != NULL) {
        play_queued(playlist);
    }
    else if (!g_list_next(plist_pos_list)) {
        if (cfg.shuffle) {
            playlist->position = NULL;
            playlist_generate_shuffle_list_nolock(playlist);
        }
        else if (playlist->entries != NULL)
            playlist->position = playlist->entries->data;

        if (!cfg.repeat) {
            PLAYLIST_UNLOCK(playlist);
            hook_call("playlist end reached", NULL);
            return;
        }
    }
    else
        playlist->position = g_list_next(plist_pos_list)->data;

    PLAYLIST_UNLOCK(playlist);

    playlist_check_pos_current(playlist);
    playback_initiate();
    hook_call("playlist update", playlist);
}

gint
playlist_queue_get_length(Playlist *playlist)
{
    gint length;

    PLAYLIST_LOCK(playlist);
    length = g_list_length(playlist->queue);
    PLAYLIST_UNLOCK(playlist);

    return length;
}

gint
playlist_get_length(Playlist *playlist)
{
    return g_list_length(playlist->entries);
}

gchar *
playlist_get_info_text(Playlist *playlist)
{
    gchar *text, *title, *numbers, *length;

    g_return_val_if_fail(playlist != NULL, NULL);

    PLAYLIST_LOCK(playlist);
    if (!playlist->position) {
        PLAYLIST_UNLOCK(playlist);
        return NULL;
    }

    /* FIXME: there should not be a need to do additional conversion,
     * if playlist is properly maintained */
    if (playlist->position->title) {
        title = str_to_utf8(playlist->position->title);
    }
    else {
        gchar *realfn = g_filename_from_uri(playlist->position->filename, NULL, NULL);
        gchar *basename = g_path_get_basename(realfn ? realfn : playlist->position->filename);
        title = filename_to_utf8(basename);
        g_free(realfn);
        g_free(basename);
    }

    /*
     * If the user don't want numbers in the playlist, don't
     * display them in other parts of XMMS
     */

    if (cfg.show_numbers_in_pl)
        numbers = g_strdup_printf("%d. ", playlist_get_position_nolock(playlist) + 1);
    else
        numbers = g_strdup("");

    if (playlist->position->length != -1)
        length = g_strdup_printf(" (%d:%-2.2d)",
                                 playlist->position->length / 60000,
                                 (playlist->position->length / 1000) % 60);
    else
        length = g_strdup("");

    PLAYLIST_UNLOCK(playlist);

    text = convert_title_text(g_strconcat(numbers, title, length, NULL));

    g_free(numbers);
    g_free(title);
    g_free(length);

    return text;
}

gint
playlist_get_current_length(Playlist * playlist)
{
    gint len = 0;

    if (!playlist)
        return 0;

    if (playlist->position)
        len = playlist->position->length;

    return len;
}

gboolean
playlist_save(Playlist * playlist, const gchar * filename)
{
    PlaylistContainer *plc = NULL;
    GList *old_iter;
    gchar *ext;

    g_return_val_if_fail(playlist != NULL, FALSE);
    g_return_val_if_fail(filename != NULL, FALSE);

    ext = strrchr(filename, '.') + 1;

    if ((plc = playlist_container_find(ext)) == NULL)
        return FALSE;

    if (plc->plc_write == NULL)
        return FALSE;

    /* Save the right playlist to disk */
    if (playlist != playlist_get_active()) {
        old_iter = playlists_iter;
        playlists_iter = g_list_find(playlists, playlist);
        if(!playlists_iter) playlists_iter = old_iter;
        plc->plc_write(filename, 0);
        playlists_iter = old_iter;
    } else {
        plc->plc_write(filename, 0);
    }

    return TRUE;
}

gboolean
playlist_load(Playlist * playlist, const gchar * filename)
{
    guint ret = 0;
    g_return_val_if_fail(playlist != NULL, FALSE);

    playlist->loading_playlist = TRUE;
    if(!playlist_get_length(playlist)) {
        /* Loading new playlist */
        playlist_filename_set(playlist, filename);
    }
    ret = playlist_load_ins(playlist, filename, -1);
    playlist->loading_playlist = FALSE;

    return ret ? TRUE : FALSE;
}

void
playlist_load_ins_file(Playlist *playlist,
                       const gchar * uri,
                       const gchar * playlist_name, gint pos,
                       const gchar * title, gint len)
{
    ProbeResult *pr = NULL;

    g_return_if_fail(uri != NULL);
    g_return_if_fail(playlist_name != NULL);
    g_return_if_fail(playlist != NULL);

    if(do_precheck(playlist, uri, &pr)) {
        __playlist_ins_file(playlist, uri, pos, NULL, title, len, pr ? pr->ip : NULL);
    }
    g_free(pr);
}

void
playlist_load_ins_file_tuple(Playlist * playlist,
                             const gchar * uri,
                             const gchar * playlist_name,   //path of playlist file itself
                             gint pos,
                             Tuple *tuple)
{
    ProbeResult *pr = NULL;		/* for decoder cache */

    g_return_if_fail(uri != NULL);
    g_return_if_fail(playlist_name != NULL);
    g_return_if_fail(playlist != NULL);

    if(do_precheck(playlist, uri, &pr)) {
        __playlist_ins_file(playlist, uri, pos, tuple, NULL, -1, pr ? pr->ip : NULL);
    }
    g_free(pr);

}

static gboolean
do_precheck(Playlist *playlist, const gchar *uri, ProbeResult **pr)
{
    gint ext_flag = filter_by_extension(uri);
    gboolean http_flag = is_http(uri);
    gboolean rv = FALSE;

    /* playlist file or remote uri */
    if ((playlist->loading_playlist == TRUE && ext_flag != EXT_HAVE_SUBTUNE ) || http_flag == TRUE) {
        pr = NULL;
        rv = TRUE;
    }
    /* local file and on-demand probing is on */
    else if (cfg.playlist_detect == TRUE && ext_flag != EXT_HAVE_SUBTUNE && ext_flag != EXT_CUSTOM) {
        if(cfg.use_extension_probing && ext_flag == EXT_FALSE) {
            AUDDBG("reject %s\n", uri);
            rv = FALSE;
        }
        else {
            pr = NULL;
            rv = TRUE;
        }
    }
    /* find decorder for local file */
    else {
        *pr = input_check_file(uri, TRUE);
        if(*pr) {
            AUDDBG("got pr\n");
            rv = TRUE;
        }
    }

    return rv;
}

static guint
playlist_load_ins(Playlist * playlist, const gchar * filename, gint pos)
{
    PlaylistContainer *plc;
    GList *old_iter;
    gchar *ext;
    gint old_len, new_len;

    g_return_val_if_fail(playlist != NULL, 0);
    g_return_val_if_fail(filename != NULL, 0);

    ext = strrchr(filename, '.') + 1;
    plc = playlist_container_find(ext);

    g_return_val_if_fail(plc != NULL, 0);
    g_return_val_if_fail(plc->plc_read != NULL, 0);

    old_len = playlist_get_length(playlist);
    /* make sure it adds files to the right playlist */
    if (playlist != playlist_get_active()) {
        old_iter = playlists_iter;
        playlists_iter = g_list_find(playlists, playlist);
        if (!playlists_iter) playlists_iter = old_iter;
        plc->plc_read(filename, pos);
        playlists_iter = old_iter;
    } else {
        plc->plc_read(filename, pos);
    }
    new_len = playlist_get_length(playlist);

    playlist_generate_shuffle_list(playlist);
    hook_call("playlist update", playlist);

    playlist_recalc_total_time(playlist); //tentative --yaz
    PLAYLIST_INCR_SERIAL(playlist);

    return new_len - old_len;
}

GList *
get_playlist_nth(Playlist *playlist, guint nth)
{
    g_warning("deprecated function get_playlist_nth() was called");
    REQUIRE_LOCK(playlist->mutex);
    return g_list_nth(playlist->entries, nth);
}

gint
playlist_get_position_nolock(Playlist *playlist)
{
    if (playlist && playlist->position)
        return g_list_index(playlist->entries, playlist->position);
    return 0;
}

gint
playlist_get_position(Playlist *playlist)
{
    gint pos;

    PLAYLIST_LOCK(playlist);
    pos = playlist_get_position_nolock(playlist);
    PLAYLIST_UNLOCK(playlist);

    return pos;
}

gchar *
playlist_get_filename(Playlist *playlist, guint pos)
{
    gchar *filename;
    PlaylistEntry *entry;
    GList *node;

    if (!playlist)
        return NULL;

    PLAYLIST_LOCK(playlist);
    node = g_list_nth(playlist->entries, pos);
    if (!node) {
        PLAYLIST_UNLOCK(playlist);
        return NULL;
    }
    entry = node->data;

    filename = g_strdup(entry->filename);
    PLAYLIST_UNLOCK(playlist);

    return filename;
}

gchar *
playlist_get_songtitle(Playlist *playlist, guint pos)
{
    gchar *title = NULL;
    PlaylistEntry *entry;
    GList *node;
    time_t mtime;

    if (!playlist)
        return NULL;

    PLAYLIST_LOCK(playlist);

    if (!(node = g_list_nth(playlist->entries, pos))) {
        PLAYLIST_UNLOCK(playlist);
        return NULL;
    }

    entry = node->data;

    if (entry->tuple)
        mtime = tuple_get_int(entry->tuple, FIELD_MTIME, NULL);
    else
        mtime = 0;

    /* FIXME: simplify this logic */
    if ((entry->title == NULL && entry->length == -1) ||
        (entry->tuple && mtime != 0 &&
         (mtime == -1 || mtime != playlist_get_mtime(entry->filename))))
    {
        if (playlist_entry_get_info(entry))
            title = entry->title;
    }
    else {
        title = entry->title;
    }

    PLAYLIST_UNLOCK(playlist);

    if (!title) {
        gchar *realfn = NULL;
        realfn = g_filename_from_uri(entry->filename, NULL, NULL);
        title = g_path_get_basename(realfn ? realfn : entry->filename);
        g_free(realfn); realfn = NULL;
        return str_replace(title, filename_to_utf8(title));
    }

    return str_to_utf8(title);
}

Tuple *
playlist_get_tuple(Playlist *playlist, guint pos)
{
    PlaylistEntry *entry;
    Tuple *tuple = NULL;
    GList *node;
    time_t mtime;

    if (!playlist)
        return NULL;

    if (!(node = g_list_nth(playlist->entries, pos))) {
        return NULL;
    }

    entry = (PlaylistEntry *) node->data;

    tuple = entry->tuple;

    if (tuple)
        mtime = tuple_get_int(tuple, FIELD_MTIME, NULL);
    else
        mtime = 0;

    // if no tuple or tuple with old mtime, get new one.
    if (tuple == NULL || 
        (entry->tuple && mtime != 0 && (mtime == -1 || mtime != playlist_get_mtime(entry->filename))))
    {
        playlist_entry_get_info(entry);
        tuple = entry->tuple;
    }

    return tuple;
}

gint
playlist_get_songtime(Playlist *playlist, guint pos)
{
    gint song_time = -1;
    PlaylistEntry *entry;
    GList *node;
    time_t mtime;

    if (!playlist)
        return -1;

    if (!(node = g_list_nth(playlist->entries, pos)))
        return -1;

    entry = node->data;

    if (entry->tuple)
        mtime = tuple_get_int(entry->tuple, FIELD_MTIME, NULL);
    else
        mtime = 0;

    if (entry->tuple == NULL ||
        (mtime != 0 && (mtime == -1 || mtime != playlist_get_mtime(entry->filename)))) {

        if (playlist_entry_get_info(entry))
            song_time = entry->length;
    } else
        song_time = entry->length;

    return song_time;
}

static gint
playlist_compare_track(PlaylistEntry * a, PlaylistEntry * b)
{
    gint tracknumber_a;
    gint tracknumber_b;

    g_return_val_if_fail(a != NULL, 0);
    g_return_val_if_fail(b != NULL, 0);

    if(!a->tuple)
        playlist_entry_get_info(a);
    if(!b->tuple)
        playlist_entry_get_info(b);

    if (a->tuple == NULL)
        return 0;
    if (b->tuple == NULL)
        return 0;

    tracknumber_a = tuple_get_int(a->tuple, FIELD_TRACK_NUMBER, NULL);
    tracknumber_b = tuple_get_int(b->tuple, FIELD_TRACK_NUMBER, NULL);

    return (tracknumber_a && tracknumber_b ?
            tracknumber_a - tracknumber_b : 0);
}

static void
playlist_get_entry_title(PlaylistEntry * e, const gchar ** title)
{
    if (e->title)
        *title = e->title;
    else {
        if (strrchr(e->filename, '/'))
            *title = strrchr(e->filename, '/') + 1;
        else
            *title = e->filename;
    }
}

static gint
playlist_compare_playlist(PlaylistEntry * a, PlaylistEntry * b)
{
    const gchar *a_title = NULL, *b_title = NULL;

    g_return_val_if_fail(a != NULL, 0);
    g_return_val_if_fail(b != NULL, 0);

    playlist_get_entry_title(a, &a_title);
    playlist_get_entry_title(b, &b_title);

    return strcasecmp(a_title, b_title);
}

static gint
playlist_compare_title(PlaylistEntry * a,
                       PlaylistEntry * b)
{
    const gchar *a_title = NULL, *b_title = NULL;

    g_return_val_if_fail(a != NULL, 0);
    g_return_val_if_fail(b != NULL, 0);

    if (a->tuple == NULL)
        playlist_entry_get_info(a);
    if (b->tuple == NULL)
        playlist_entry_get_info(b);

    if (a->tuple != NULL)
        a_title = tuple_get_string(a->tuple, FIELD_TITLE, NULL);

    if (b->tuple != NULL)
        b_title = tuple_get_string(b->tuple, FIELD_TITLE, NULL);

    if (a_title != NULL && b_title != NULL)
        return strcasecmp(a_title, b_title);

    playlist_get_entry_title(a, &a_title);
    playlist_get_entry_title(b, &b_title);

    return strcasecmp(a_title, b_title);
}

static gint
playlist_compare_artist(PlaylistEntry * a,
                        PlaylistEntry * b)
{
    const gchar *a_artist = NULL, *b_artist = NULL;

    g_return_val_if_fail(a != NULL, 0);
    g_return_val_if_fail(b != NULL, 0);

    if (a->tuple != NULL)
        playlist_entry_get_info(a);

    if (b->tuple != NULL)
        playlist_entry_get_info(b);

    if (a->tuple != NULL) {
        a_artist = tuple_get_string(a->tuple, FIELD_ARTIST, NULL);

        if (a_artist == NULL)
            return 0;

        if (str_has_prefix_nocase(a_artist, "the "))
            a_artist += 4;
    }

    if (b->tuple != NULL) {
        b_artist = tuple_get_string(b->tuple, FIELD_ARTIST, NULL);

        if (b_artist == NULL)
            return 0;

        if (str_has_prefix_nocase(b_artist, "the "))
            b_artist += 4;
    }

    if (a_artist != NULL && b_artist != NULL)
        return strcasecmp(a_artist, b_artist);

    return 0;
}

static gint
playlist_compare_filename(PlaylistEntry * a,
                          PlaylistEntry * b)
{
    gchar *a_filename, *b_filename;

    g_return_val_if_fail(a != NULL, 0);
    g_return_val_if_fail(b != NULL, 0);

    if (strrchr(a->filename, '/'))
        a_filename = strrchr(a->filename, '/') + 1;
    else
        a_filename = a->filename;

    if (strrchr(b->filename, '/'))
        b_filename = strrchr(b->filename, '/') + 1;
    else
        b_filename = b->filename;


    return strcasecmp(a_filename, b_filename);
}

static gint
path_compare(const gchar * a, const gchar * b)
{
    gchar *posa, *posb;
    gint len, ret;

    posa = strrchr(a, '/');
    posb = strrchr(b, '/');

    /*
     * Sort directories before files
     */
    if (posa && posb && (posa - a != posb - b)) {
        if (posa - a > posb - b) {
            len = posb - b;
            ret = -1;
        }
        else {
            len = posa - a;
            ret = 1;
        }
        if (!strncasecmp(a, b, len))
            return ret;
    }
    return strcasecmp(a, b);
}

static gint
playlist_compare_path(PlaylistEntry * a,
                      PlaylistEntry * b)
{
    return path_compare(a->filename, b->filename);
}


static time_t
playlist_get_mtime(const gchar *filename)
{
    struct stat buf;
    gint rv;
    gchar *realfn = NULL;

    /* stat() does not accept file:// --yaz */
    realfn = g_filename_from_uri(filename, NULL, NULL);
    rv = stat(realfn ? realfn : filename, &buf);
    g_free(realfn); realfn = NULL;

    if (rv == 0) {
        return buf.st_mtime;
    } else {
        return 0; //error
    }
}


static gint
playlist_compare_date(PlaylistEntry * a,
                      PlaylistEntry * b)
{
    struct stat buf;
    time_t modtime;

    gint rv;


    rv = stat(a->filename, &buf);

    if (rv == 0) {
        modtime = buf.st_mtime;
        rv = stat(b->filename, &buf);

        if (stat(b->filename, &buf) == 0) {
            if (buf.st_mtime == modtime)
                return 0;
            else
                return (buf.st_mtime - modtime) > 0 ? -1 : 1;
        }
        else
            return -1;
    }
    else if (!lstat(b->filename, &buf))
        return 1;
    else
        return playlist_compare_filename(a, b);
}


void
playlist_sort(Playlist *playlist, PlaylistSortType type)
{
    playlist_remove_dead_files(playlist);
    PLAYLIST_LOCK(playlist);
    playlist->entries =
        g_list_sort(playlist->entries,
                    (GCompareFunc) playlist_compare_func_table[type]);
    PLAYLIST_INCR_SERIAL(playlist);
    PLAYLIST_UNLOCK(playlist);
}

static GList *
playlist_sort_selected_generic(GList * list, GCompareFunc cmpfunc)
{
    GList *list1, *list2;
    GList *tmp_list = NULL;
    GList *index_list = NULL;

    /*
     * We take all the selected entries out of the playlist,
     * sorts them, and then put them back in again.
     */

    list1 = g_list_last(list);

    while (list1) {
        list2 = g_list_previous(list1);
        if (PLAYLIST_ENTRY(list1->data)->selected) {
            gpointer idx;
            idx = GINT_TO_POINTER(g_list_position(list, list1));
            index_list = g_list_prepend(index_list, idx);
            list = g_list_remove_link(list, list1);
            tmp_list = g_list_concat(list1, tmp_list);
        }
        list1 = list2;
    }

    tmp_list = g_list_sort(tmp_list, cmpfunc);
    list1 = tmp_list;
    list2 = index_list;

    while (list2) {
        if (!list1) {
            g_critical(G_STRLOC ": Error during list sorting. "
                       "Possibly dropped some playlist-entries.");
            break;
        }

        list = g_list_insert(list, list1->data, GPOINTER_TO_INT(list2->data));

        list2 = g_list_next(list2);
        list1 = g_list_next(list1);
    }

    g_list_free(index_list);
    g_list_free(tmp_list);

    return list;
}

void
playlist_sort_selected(Playlist *playlist, PlaylistSortType type)
{
    PLAYLIST_LOCK(playlist);
    playlist->entries = playlist_sort_selected_generic(playlist->entries, (GCompareFunc)
                                                       playlist_compare_func_table
                                                       [type]);
    PLAYLIST_INCR_SERIAL(playlist);
    PLAYLIST_UNLOCK(playlist);
}

void
playlist_reverse(Playlist *playlist)
{
    PLAYLIST_LOCK(playlist);
    playlist->entries = g_list_reverse(playlist->entries);
    PLAYLIST_INCR_SERIAL(playlist);
    PLAYLIST_UNLOCK(playlist);
}

static GList *
playlist_shuffle_list(Playlist *playlist, GList * list)
{
    /*
     * Note that this doesn't make a copy of the original list.
     * The pointer to the original list is not valid after this
     * fuction is run.
     */
    gint len = g_list_length(list);
    gint i, j;
    GList *node, **ptrs;

    if (!playlist)
        return NULL;

    REQUIRE_LOCK(playlist->mutex);

    if (!len)
        return NULL;

    ptrs = g_new(GList *, len);

    for (node = list, i = 0; i < len; node = g_list_next(node), i++)
        ptrs[i] = node;

    j = g_random_int_range(0, len);
    list = ptrs[j];
    ptrs[j]->next = NULL;
    ptrs[j] = ptrs[0];

    for (i = 1; i < len; i++) {
        j = g_random_int_range(0, len - i);
        list->prev = ptrs[i + j];
        ptrs[i + j]->next = list;
        list = ptrs[i + j];
        ptrs[i + j] = ptrs[i];
    }
    list->prev = NULL;

    g_free(ptrs);

    return list;
}

void
playlist_random(Playlist *playlist)
{
    PLAYLIST_LOCK(playlist);
    playlist->entries = playlist_shuffle_list(playlist, playlist->entries);
    PLAYLIST_INCR_SERIAL(playlist);
    PLAYLIST_UNLOCK(playlist);
}

GList *
playlist_get_selected(Playlist *playlist)
{
    GList *node, *list = NULL;
    gint i = 0;

    PLAYLIST_LOCK(playlist);
    for (node = playlist->entries; node; node = g_list_next(node), i++) {
        PlaylistEntry *entry = node->data;
        if (entry->selected)
            list = g_list_prepend(list, GINT_TO_POINTER(i));
    }
    PLAYLIST_UNLOCK(playlist);
    return g_list_reverse(list);
}

void
playlist_clear_selected(Playlist *playlist)
{
    GList *node = NULL;
    gint i = 0;

    PLAYLIST_LOCK(playlist);
    for (node = playlist->entries; node; node = g_list_next(node), i++) {
        PLAYLIST_ENTRY(node->data)->selected = FALSE;
    }
    PLAYLIST_INCR_SERIAL(playlist);
    PLAYLIST_UNLOCK(playlist);
    playlist_recalc_total_time(playlist);
    hook_call("playlist update", playlist);
}

gint
playlist_get_num_selected(Playlist *playlist)
{
    GList *node;
    gint num = 0;

    PLAYLIST_LOCK(playlist);
    for (node = playlist->entries; node; node = g_list_next(node)) {
        PlaylistEntry *entry = node->data;
        if (entry->selected)
            num++;
    }
    PLAYLIST_UNLOCK(playlist);
    return num;
}


static void
playlist_generate_shuffle_list(Playlist *playlist)
{
    PLAYLIST_LOCK(playlist);
    playlist_generate_shuffle_list_nolock(playlist);
    PLAYLIST_UNLOCK(playlist);
}

static void
playlist_generate_shuffle_list_nolock(Playlist *playlist)
{
    GList *node;
    gint numsongs;

    if (!cfg.shuffle || !playlist)
        return;

    REQUIRE_LOCK(playlist->mutex);

    if (playlist->shuffle) {
        g_list_free(playlist->shuffle);
        playlist->shuffle = NULL;
    }

    playlist->shuffle = playlist_shuffle_list(playlist, g_list_copy(playlist->entries));
    numsongs = g_list_length(playlist->shuffle);

    if (playlist->position) {
        gint i = g_list_index(playlist->shuffle, playlist->position);
        node = g_list_nth(playlist->shuffle, i);
        playlist->shuffle = g_list_remove_link(playlist->shuffle, node);
        playlist->shuffle = g_list_prepend(playlist->shuffle, node->data);
    }
}


static gboolean
playlist_get_info_is_going(void)
{
    gboolean result;

    g_static_rw_lock_reader_lock(&playlist_get_info_rwlock);
    result = playlist_get_info_going;
    g_static_rw_lock_reader_unlock(&playlist_get_info_rwlock);

    return result;
}


static gpointer
playlist_get_info_func(gpointer arg)
{
    GList *node;
    gboolean update_playlistwin = FALSE;

    while (playlist_get_info_is_going()) {
        PlaylistEntry *entry;
        Playlist *playlist = playlist_get_active();

        // on_load
        if (cfg.use_pl_metadata && cfg.get_info_on_load &&
            playlist_get_info_scan_active) {

            for (node = playlist->entries; node; node = g_list_next(node)) {
                entry = node->data;

                if (playlist->attribute & PLAYLIST_STATIC || // live lock fix
                    (entry->tuple &&
                     tuple_get_int(entry->tuple, FIELD_LENGTH, NULL) > -1 &&
                     tuple_get_int(entry->tuple, FIELD_MTIME, NULL) != -1 &&
                     entry->title_is_valid))
                {
                    update_playlistwin = TRUE;
                    continue;
                }

                if (!playlist_entry_get_info(entry) && 
                    g_list_index(playlist->entries, entry) == -1)
                    /* Entry disappeared while we looked it up.
                       Restart. */
                    node = playlist->entries;
                else if ((entry->tuple != NULL || entry->title != NULL) && 
                         tuple_get_int(entry->tuple, FIELD_LENGTH, NULL) > -1 &&
                         tuple_get_int(entry->tuple, FIELD_MTIME, NULL) != -1)
                {
                    update_playlistwin = TRUE;
                    break; /* hmmm... --asphyx */
                }
            }

            if (!node) {
                g_mutex_lock(mutex_scan);
                playlist_get_info_scan_active = FALSE;
                g_mutex_unlock(mutex_scan);
            }
        } // on_load

        // on_demand
        else if (!cfg.get_info_on_load &&
                 cfg.get_info_on_demand &&
                 cfg.playlist_visible &&
                 !cfg.playlist_shaded &&
                 cfg.use_pl_metadata) {

            g_mutex_lock(mutex_scan);
            playlist_get_info_scan_active = FALSE;
            g_mutex_unlock(mutex_scan);

            for (node = g_list_nth(playlist->entries, playlistwin_get_toprow());
                 node && playlistwin_item_visible(g_list_position(playlist->entries, node));
                 node = g_list_next(node)) {

                entry = node->data;

                if (playlist->attribute & PLAYLIST_STATIC ||
                    (entry->tuple &&
                     tuple_get_int(entry->tuple, FIELD_LENGTH, NULL) > -1 &&
                     tuple_get_int(entry->tuple, FIELD_MTIME, NULL) != -1 &&
                     entry->title_is_valid))
                    continue;

                AUDDBG("len=%d mtime=%d\n",
                       tuple_get_int(entry->tuple, FIELD_LENGTH, NULL),
                       tuple_get_int(entry->tuple, FIELD_MTIME, NULL));

                if (!playlist_entry_get_info(entry)) { 
                    if (g_list_index(playlist->entries, entry) == -1)
                        /* Entry disapeared while we
                           looked it up.  Restart. */
                        node = g_list_nth(playlist->entries,
                                          playlistwin_get_toprow());
                }
                else if ((entry->tuple != NULL || entry->title != NULL) && 
                         tuple_get_int(entry->tuple, FIELD_LENGTH, NULL) > -1 &&
                         tuple_get_int(entry->tuple, FIELD_MTIME, NULL) != -1) {
                    update_playlistwin = TRUE;
                }
            }
        } // on_demand

        else if (cfg.get_info_on_demand &&
                 (!cfg.playlist_visible || cfg.playlist_shaded || !cfg.use_pl_metadata))
        {
            g_mutex_lock(mutex_scan);
            playlist_get_info_scan_active = FALSE;
            g_mutex_unlock(mutex_scan);
        }

        else /* not on_demand and not on_load...
                NOTE: this shouldn't happen anymore, sanity check in aud_config_load now */
        {
            g_mutex_lock(mutex_scan);
            playlist_get_info_scan_active = FALSE;
            g_mutex_unlock(mutex_scan);
        }

        if (update_playlistwin) {
            Playlist *playlist = playlist_get_active();
            event_queue("playlist update", playlist);
            PLAYLIST_INCR_SERIAL(playlist);
            update_playlistwin = FALSE;
        }

        if (playlist_get_info_scan_active)
            continue;

        g_mutex_lock(mutex_scan);
        g_cond_wait(cond_scan, mutex_scan);
        g_mutex_unlock(mutex_scan);

        // AUDDBG("scanner invoked\n");

    } // while

    g_thread_exit(NULL);
    return NULL;
}

void
playlist_start_get_info_thread(void)
{
    g_static_rw_lock_writer_lock(&playlist_get_info_rwlock);
    playlist_get_info_going = TRUE;
    g_static_rw_lock_writer_unlock(&playlist_get_info_rwlock);

    playlist_get_info_thread = g_thread_create(playlist_get_info_func,
                                               NULL, TRUE, NULL);
}

void
playlist_stop_get_info_thread(void)
{
    g_static_rw_lock_writer_lock(&playlist_get_info_rwlock);
    if (!playlist_get_info_going) {
        g_static_rw_lock_writer_unlock(&playlist_get_info_rwlock);
        return;
    }
    
    playlist_get_info_going = FALSE;
    g_static_rw_lock_writer_unlock(&playlist_get_info_rwlock);

    g_cond_broadcast(cond_scan);
    g_thread_join(playlist_get_info_thread);
}

void
playlist_start_get_info_scan(void)
{
    AUDDBG("waking up scan thread\n");
    g_mutex_lock(mutex_scan);
    playlist_get_info_scan_active = TRUE;
    g_mutex_unlock(mutex_scan);

    g_cond_signal(cond_scan);
}

void
playlist_update_all_titles(void) /* update titles after format changing --asphyx */
{
    PlaylistEntry *entry;
    GList *node;
    Playlist *playlist = playlist_get_active();

    AUDDBG("invalidating titles\n");
    PLAYLIST_LOCK(playlist);
    for (node = playlist->entries; node; node = g_list_next(node)) {
        entry = node->data;
        entry->title_is_valid = FALSE;
    }
    PLAYLIST_UNLOCK(playlist);
    playlist_start_get_info_scan();
}

void
playlist_remove_dead_files(Playlist *playlist)
{
    GList *node, *next_node;

    PLAYLIST_LOCK(playlist);

    for (node = playlist->entries; node; node = next_node) {
        PlaylistEntry *entry = PLAYLIST_ENTRY(node->data);
        next_node = g_list_next(node);

        if (!entry || !entry->filename) {
            g_message(G_STRLOC ": Playlist entry is invalid!");
            continue;
        }

        if (!g_str_has_prefix(entry->filename, "file://"))
            continue;

        /* FIXME: Should test for readability */
        if (vfs_file_test(entry->filename, G_FILE_TEST_EXISTS))  
            continue;

        if (entry == playlist->position) {
            /* Don't remove the currently playing song */
            if (playback_get_playing())
                continue;

            if (next_node)
                playlist->position = PLAYLIST_ENTRY(next_node->data);
            else
                playlist->position = NULL;
        }

        playlist_entry_free(entry);
        playlist->entries = g_list_delete_link(playlist->entries, node);
    }

    PLAYLIST_UNLOCK(playlist);

    playlist_generate_shuffle_list(playlist);
    hook_call("playlist update", playlist);
    playlist_recalc_total_time(playlist);
    PLAYLIST_INCR_SERIAL(playlist);
}


static gint
playlist_dupscmp_title(PlaylistEntry * a,
                       PlaylistEntry * b)
{
    const gchar *a_title, *b_title;

    g_return_val_if_fail(a != NULL, 0);
    g_return_val_if_fail(b != NULL, 0);

    playlist_get_entry_title(a, &a_title);
    playlist_get_entry_title(b, &b_title);

    return strcmp(a_title, b_title);
}

static gint
playlist_dupscmp_filename(PlaylistEntry * a,
                          PlaylistEntry * b )
{
    gchar *a_filename, *b_filename;

    g_return_val_if_fail(a != NULL, 0);
    g_return_val_if_fail(b != NULL, 0);

    if (strrchr(a->filename, '/'))
        a_filename = strrchr(a->filename, '/') + 1;
    else
        a_filename = a->filename;

    if (strrchr(b->filename, '/'))
        b_filename = strrchr(b->filename, '/') + 1;
    else
        b_filename = b->filename;

    return strcmp(a_filename, b_filename);
}

static gint
playlist_dupscmp_path(PlaylistEntry * a,
                      PlaylistEntry * b)
{
    /* simply compare the entire filename string */
    return strcmp(a->filename, b->filename);
}

void
playlist_remove_duplicates(Playlist *playlist, PlaylistDupsType type)
{
    GList *node, *next_node;
    GList *node_cmp, *next_node_cmp;
    gint (*dups_compare_func)(PlaylistEntry * , PlaylistEntry *);

    switch ( type )
    {
        case PLAYLIST_DUPS_TITLE:
            dups_compare_func = playlist_dupscmp_title;
            break;
        case PLAYLIST_DUPS_PATH:
            dups_compare_func = playlist_dupscmp_path;
            break;
        case PLAYLIST_DUPS_FILENAME:
        default:
            dups_compare_func = playlist_dupscmp_filename;
            break;
    }

    PLAYLIST_LOCK(playlist);

    for (node = playlist->entries; node; node = next_node) {
        PlaylistEntry *entry = PLAYLIST_ENTRY(node->data);
        next_node = g_list_next(node);

        if (!entry || !entry->filename) {
            g_message(G_STRLOC ": Playlist entry is invalid!");
            continue;
        }

        for (node_cmp = next_node; node_cmp; node_cmp = next_node_cmp) {
            PlaylistEntry *entry_cmp = PLAYLIST_ENTRY(node_cmp->data);
            next_node_cmp = g_list_next(node_cmp);

            if (!entry_cmp || !entry_cmp->filename) {
                g_message(G_STRLOC ": Playlist entry is invalid!");
                continue;
            }

            /* compare using the chosen dups_compare_func */
            if ( !dups_compare_func( entry , entry_cmp ) ) {

                if (entry_cmp == playlist->position) {
                    /* Don't remove the currently playing song */
                    if (playback_get_playing())
                        continue;

                    if (next_node_cmp)
                        playlist->position = PLAYLIST_ENTRY(next_node_cmp->data);
                    else
                        playlist->position = NULL;
                }

                /* check if this was the next item of the external
                   loop; if true, replace it with the next of the next*/
                if ( node_cmp == next_node )
                    next_node = g_list_next(next_node);

                playlist_entry_free(entry_cmp);
                playlist->entries = g_list_delete_link(playlist->entries, node_cmp);
            }
        }
    }

    PLAYLIST_UNLOCK(playlist);

    hook_call("playlist update", playlist);
    playlist_recalc_total_time(playlist);
    PLAYLIST_INCR_SERIAL(playlist);
}

void
playlist_get_total_time(Playlist * playlist,
                        gulong * total_time,
                        gulong * selection_time,
                        gboolean * total_more,
                        gboolean * selection_more)
{
    PLAYLIST_LOCK(playlist);
    *total_time = playlist->pl_total_time;
    *selection_time = playlist->pl_selection_time;
    *total_more = playlist->pl_total_more;
    *selection_more = playlist->pl_selection_more;
    PLAYLIST_UNLOCK(playlist);
}

static void
playlist_recalc_total_time_nolock(Playlist *playlist)
{
    GList *list;
    PlaylistEntry *entry;

    REQUIRE_LOCK(playlist->mutex);

    playlist->pl_total_time = 0;
    playlist->pl_selection_time = 0;
    playlist->pl_total_more = FALSE;
    playlist->pl_selection_more = FALSE;

    for (list = playlist->entries; list; list = g_list_next(list)) {
        entry = list->data;

        if (entry->length != -1)
            playlist->pl_total_time += entry->length / 1000;
        else
            playlist->pl_total_more = TRUE;

        if (entry->selected) {
            if (entry->length != -1)
                playlist->pl_selection_time += entry->length / 1000;
            else
                playlist->pl_selection_more = TRUE;
        }
    }
}

static void
playlist_recalc_total_time(Playlist *playlist)
{
    PLAYLIST_LOCK(playlist);
    playlist_recalc_total_time_nolock(playlist);
    PLAYLIST_UNLOCK(playlist);
}

gint
playlist_select_search( Playlist *playlist , Tuple *tuple , gint action )
{
    GList *entry_list = NULL, *found_list = NULL, *sel_list = NULL;
    gboolean is_first_search = TRUE;
    gint num_of_entries_found = 0;
    const gchar *regex_pattern;
    const gchar *track_name;
    const gchar *album_name;
    const gchar *performer;
    const gchar *file_name;

#if defined(USE_REGEX_ONIGURUMA)
    /* set encoding for Oniguruma regex to UTF-8 */
    reg_set_encoding( REG_POSIX_ENCODING_UTF8 );
    onig_set_default_syntax( ONIG_SYNTAX_POSIX_BASIC );
#endif

    PLAYLIST_LOCK(playlist);

    if ( (regex_pattern = tuple_get_string(tuple, FIELD_TITLE, NULL)) != NULL &&
         (*regex_pattern != '\0') )
    {
        /* match by track_name */
        regex_t regex;
#if defined(USE_REGEX_PCRE)
        if ( regcomp( &regex , regex_pattern , REG_NOSUB | REG_ICASE | REG_UTF8 ) == 0 )
#else
            if ( regcomp( &regex , regex_pattern , REG_NOSUB | REG_ICASE ) == 0 )
#endif
            {
                GList *tfound_list = NULL;
                if ( is_first_search == TRUE ) entry_list = playlist->entries;
                else entry_list = found_list; /* use found_list */
                for ( ; entry_list ; entry_list = g_list_next(entry_list) )
                {
                    PlaylistEntry *entry = entry_list->data;
                    if ( entry->tuple != NULL )
                    {
                        track_name = tuple_get_string( entry->tuple, FIELD_TITLE, NULL );
                        if (( track_name != NULL ) &&
                            ( regexec( &regex , track_name , 0 , NULL , 0 ) == 0 ) )
                        {
                            tfound_list = g_list_append( tfound_list , entry );
                        }
                    }
                }
                g_list_free( found_list ); /* wipe old found_list */
                found_list = tfound_list; /* move tfound_list in found_list */
                regfree( &regex );
            }
        is_first_search = FALSE;
    }

    if ( (regex_pattern = tuple_get_string(tuple, FIELD_ALBUM, NULL)) != NULL &&
         (*regex_pattern != '\0') )
    {
        /* match by album_name */
        regex_t regex;
#if defined(USE_REGEX_PCRE)
        if ( regcomp( &regex , regex_pattern , REG_NOSUB | REG_ICASE | REG_UTF8 ) == 0 )
#else
            if ( regcomp( &regex , regex_pattern , REG_NOSUB | REG_ICASE ) == 0 )
#endif
            {
                GList *tfound_list = NULL;
                if ( is_first_search == TRUE ) entry_list = playlist->entries;
                else entry_list = found_list; /* use found_list */
                for ( ; entry_list ; entry_list = g_list_next(entry_list) )
                {
                    PlaylistEntry *entry = entry_list->data;
                    if ( entry->tuple != NULL )
                    {
                        album_name = tuple_get_string( entry->tuple, FIELD_ALBUM, NULL );
                        if ( ( album_name != NULL ) &&
                             ( regexec( &regex , album_name , 0 , NULL , 0 ) == 0 ) )
                        {
                            tfound_list = g_list_append( tfound_list , entry );
                        }
                    }
                }
                g_list_free( found_list ); /* wipe old found_list */
                found_list = tfound_list; /* move tfound_list in found_list */
                regfree( &regex );
            }
        is_first_search = FALSE;
    }

    if ( (regex_pattern = tuple_get_string(tuple, FIELD_ARTIST, NULL)) != NULL &&
         (*regex_pattern != '\0') )
    {
        /* match by performer */
        regex_t regex;
#if defined(USE_REGEX_PCRE)
        if ( regcomp( &regex , regex_pattern , REG_NOSUB | REG_ICASE | REG_UTF8 ) == 0 )
#else
            if ( regcomp( &regex , regex_pattern , REG_NOSUB | REG_ICASE ) == 0 )
#endif
            {
                GList *tfound_list = NULL;
                if ( is_first_search == TRUE ) entry_list = playlist->entries;
                else entry_list = found_list; /* use found_list */
                for ( ; entry_list ; entry_list = g_list_next(entry_list) )
                {
                    PlaylistEntry *entry = entry_list->data;
                    if ( entry->tuple != NULL )
                    {
                        performer = tuple_get_string( entry->tuple, FIELD_ARTIST, NULL );
                        if ( ( entry->tuple != NULL ) && ( performer != NULL ) &&
                             ( regexec( &regex , performer , 0 , NULL , 0 ) == 0 ) )
                        {
                            tfound_list = g_list_append( tfound_list , entry );
                        }
                    }
                }
                g_list_free( found_list ); /* wipe old found_list */
                found_list = tfound_list; /* move tfound_list in found_list */
                regfree( &regex );
            }
        is_first_search = FALSE;
    }

    if ( (regex_pattern = tuple_get_string(tuple, FIELD_FILE_NAME, NULL)) != NULL &&
         (*regex_pattern != '\0') )
    {
        /* match by file_name */
        regex_t regex;
#if defined(USE_REGEX_PCRE)
        if ( regcomp( &regex , regex_pattern , REG_NOSUB | REG_ICASE | REG_UTF8 ) == 0 )
#else
            if ( regcomp( &regex , regex_pattern , REG_NOSUB | REG_ICASE ) == 0 )
#endif
            {
                GList *tfound_list = NULL;
                if ( is_first_search == TRUE ) entry_list = playlist->entries;
                else entry_list = found_list; /* use found_list */
                for ( ; entry_list ; entry_list = g_list_next(entry_list) )
                {
                    PlaylistEntry *entry = entry_list->data;
                    if ( entry->tuple != NULL )
                    {
                        file_name = tuple_get_string( entry->tuple, FIELD_FILE_NAME, NULL );
                        if ( ( file_name != NULL ) &&
                             ( regexec( &regex , file_name , 0 , NULL , 0 ) == 0 ) )
                        {
                            tfound_list = g_list_append( tfound_list , entry );
                        }
                    }
                }
                g_list_free( found_list ); /* wipe old found_list */
                found_list = tfound_list; /* move tfound_list in found_list */
                regfree( &regex );
            }
        is_first_search = FALSE;
    }

    /* NOTE: action = 0 -> default behaviour, select all matching entries */
    /* if some entries are still in found_list, those
       are what the user is searching for; select them */
    for ( sel_list = found_list ; sel_list ; sel_list = g_list_next(sel_list) )
    {
        PlaylistEntry *entry = sel_list->data;
        entry->selected = TRUE;
        num_of_entries_found++;
    }

    g_list_free( found_list );

    PLAYLIST_UNLOCK(playlist);
    playlist_recalc_total_time(playlist);
    //    PLAYLIST_INCR_SERIAL(playlist); //unnecessary? --yaz

    return num_of_entries_found;
}

void
playlist_select_all(Playlist *playlist, gboolean set)
{
    GList *list;

    PLAYLIST_LOCK(playlist);

    for (list = playlist->entries; list; list = g_list_next(list)) {
        PlaylistEntry *entry = list->data;
        entry->selected = set;
    }

    PLAYLIST_UNLOCK(playlist);
    playlist_recalc_total_time(playlist);
}

void
playlist_select_invert_all(Playlist *playlist)
{
    GList *list;

    PLAYLIST_LOCK(playlist);

    for (list = playlist->entries; list; list = g_list_next(list)) {
        PlaylistEntry *entry = list->data;
        entry->selected = !entry->selected;
    }

    PLAYLIST_UNLOCK(playlist);
    playlist_recalc_total_time(playlist);
}

gboolean
playlist_select_invert(Playlist *playlist, guint pos)
{
    GList *list;
    gboolean invert_ok = FALSE;

    PLAYLIST_LOCK(playlist);

    if ((list = g_list_nth(playlist->entries, pos))) {
        PlaylistEntry *entry = list->data;
        entry->selected = !entry->selected;
        invert_ok = TRUE;
    }

    PLAYLIST_UNLOCK(playlist);
    playlist_recalc_total_time(playlist);

    return invert_ok;
}


void
playlist_select_range(Playlist *playlist, gint min_pos, gint max_pos, gboolean select)
{
    GList *list;
    gint i;

    if (min_pos > max_pos)
        SWAP(min_pos, max_pos);

    PLAYLIST_LOCK(playlist);

    list = g_list_nth(playlist->entries, min_pos);
    for (i = min_pos; i <= max_pos && list; i++) {
        PlaylistEntry *entry = list->data;
        entry->selected = select;
        list = g_list_next(list);
    }

    PLAYLIST_UNLOCK(playlist);

    playlist_recalc_total_time(playlist);
}

gboolean
playlist_read_info_selection(Playlist *playlist)
{
    GList *node;
    gboolean retval = FALSE;

    PLAYLIST_LOCK(playlist);

    for (node = playlist->entries; node; node = g_list_next(node)) {
        PlaylistEntry *entry = node->data;
        if (!entry->selected)
            continue;

        retval = TRUE;

        str_replace_in(&entry->title, NULL);
        entry->length = -1;

        /* invalidate mtime to reread */
        if (entry->tuple != NULL)
            tuple_associate_int(entry->tuple, FIELD_MTIME, NULL, -1); /* -1 denotes "non-initialized". now 0 is for stream etc. yaz */

        if (!playlist_entry_get_info(entry)) {
            if (g_list_index(playlist->entries, entry) == -1)
                /* Entry disappeared while we looked it up. Restart. */
                node = playlist->entries;
        }
    }

    PLAYLIST_UNLOCK(playlist);

    hook_call("playlist update", playlist);
    playlist_recalc_total_time(playlist);
    PLAYLIST_INCR_SERIAL(playlist); //tentative --yaz

    return retval;
}

void
playlist_read_info(Playlist *playlist, guint pos)
{
    GList *node;

    PLAYLIST_LOCK(playlist);

    if ((node = g_list_nth(playlist->entries, pos))) {
        PlaylistEntry *entry = node->data;
        str_replace_in(&entry->title, NULL);
        entry->length = -1;
        playlist_entry_get_info(entry);
    }

    PLAYLIST_UNLOCK(playlist);

    hook_call("playlist update", playlist);
    playlist_recalc_total_time(playlist);
    PLAYLIST_INCR_SERIAL(playlist); //tentative --yaz
}

Playlist *
playlist_get_active(void)
{
    if (playlists_iter != NULL)
        return (Playlist *) playlists_iter->data;

    if (playlists)
        return (Playlist *) playlists->data;

    return NULL;
}

void
playlist_set_shuffle(gboolean shuffle)
{
    Playlist *playlist = playlist_get_active();
    if (!playlist)
        return;

    PLAYLIST_LOCK(playlist);

    playlist_position_before_jump = NULL;

    cfg.shuffle = shuffle;
    playlist_generate_shuffle_list_nolock(playlist);

    PLAYLIST_UNLOCK(playlist);
}

Playlist *
playlist_new(void)
{
    Playlist *playlist = g_new0(Playlist, 1);
    playlist->mutex = g_mutex_new();
    playlist->loading_playlist = FALSE;
    playlist->title = NULL;
    playlist->filename = NULL;
    playlist_clear(playlist);
    playlist->tail = NULL;
    playlist->attribute = PLAYLIST_PLAIN;
    playlist->serial = 0;

    return playlist;
}

void
playlist_free(Playlist *playlist)
{
    if (!playlist)
        return;

    if (playlist->filename)
        g_free( playlist->filename );
    g_mutex_free( playlist->mutex );
    g_free( playlist );
    playlist = NULL; //XXX lead to crash? --yaz
}

Playlist *
playlist_new_from_selected(void)
{
    Playlist *newpl = playlist_new();
    Playlist *playlist = playlist_get_active();
    GList *list = playlist_get_selected(playlist);

    playlist_add_playlist( newpl );

    PLAYLIST_LOCK(playlist);

    while ( list != NULL )
    {
        PlaylistEntry *entry = g_list_nth_data(playlist->entries, GPOINTER_TO_INT(list->data));
        if ( entry->filename != NULL ) /* paranoid? oh well... */
            playlist_add( newpl , entry->filename );
        list = g_list_next(list);
    }

    PLAYLIST_UNLOCK(playlist);

    playlist_recalc_total_time(newpl);
    hook_call("playlist update", playlist);

    return newpl;
}

const gchar *
playlist_get_filename_to_play(Playlist *playlist)
{
    const gchar *filename = NULL;

    if (!playlist)
        return NULL;

    PLAYLIST_LOCK(playlist);

    if (!playlist->position) {
        if (cfg.shuffle)
            playlist->position = playlist->shuffle->data;
        else
            playlist->position = playlist->entries->data;
    }

    filename = playlist->position->filename;

    PLAYLIST_UNLOCK(playlist);

    return filename;
}

PlaylistEntry *
playlist_get_entry_to_play(Playlist *playlist)
{
    if (!playlist)
        return NULL;

    PLAYLIST_LOCK(playlist);

    if (!playlist->position) {
        if (cfg.shuffle)
            playlist->position = playlist->shuffle->data;
        else
            playlist->position = playlist->entries->data;
    }

    PLAYLIST_UNLOCK(playlist);

    return playlist->position;
}

gboolean
playlist_playlists_equal(Playlist *p1, Playlist *p2)
{
    GList *l1, *l2;
    PlaylistEntry *e1, *e2;
    if (!p1 || !p2) return FALSE;
    l1 = p1->entries;
    l2 = p2->entries;
    do {
        if (!l1 && !l2) break;
        if (!l1 || !l2) return FALSE; /* different length */
        e1 = (PlaylistEntry *) l1->data;
        e2 = (PlaylistEntry *) l2->data;
        if (strcmp(e1->filename, e2->filename) != 0) return FALSE;
        l1 = l1->next;
        l2 = l2->next;
    } while(1);
    return TRUE;
}

static gint
filter_by_extension(const gchar *uri)
{
    gchar *base, *ext, *lext, *filename, *tmp_uri;
    gchar *tmp;
    gint rv;
    GList **lhandle, *node;
    InputPlugin *ip;

    g_return_val_if_fail(uri != NULL, EXT_FALSE);

    /* Some URIs will end in ?<subsong> to determine the subsong requested. */
    tmp_uri = g_strdup(uri);
    tmp = strrchr(tmp_uri, '?');

    if (tmp != NULL && g_ascii_isdigit(*(tmp + 1)))
        *tmp = '\0';

    /* Check for plugins with custom URI:// strings */
    /* cue:// cdda:// tone:// tact:// */
    if ((ip = uri_get_plugin(tmp_uri)) != NULL && ip->enabled) {
        g_free(tmp_uri);
        return EXT_CUSTOM;
    }

    tmp = g_filename_from_uri(tmp_uri, NULL, NULL);
    filename = g_strdup(tmp ? tmp : tmp_uri);
    g_free(tmp); tmp = NULL;
    g_free(tmp_uri); tmp_uri = NULL;


    base = g_path_get_basename(filename);
    g_free(filename);
    ext = strrchr(base, '.');

    if(!ext) {
        g_free(base);
        return EXT_FALSE;
    }

    lext = g_ascii_strdown(ext+1, -1);
    g_free(base);

    lhandle = g_hash_table_lookup(ext_hash, lext);
    g_free(lext);

    if(!lhandle) {
        return EXT_FALSE;
    }

    for(node = *lhandle; node; node = g_list_next(node)) {
        ip = (InputPlugin *)node->data;

        if(ip->have_subtune == TRUE) {
            return EXT_HAVE_SUBTUNE;
        }
        else
            rv = EXT_TRUE;
    }

    return rv;
}

static gboolean
is_http(const gchar *uri)
{
    gboolean rv = FALSE;

    if(str_has_prefix_nocase(uri, "http://") ||
       str_has_prefix_nocase(uri, "https://")) {
        rv = TRUE;
    }

    return rv;
}
