diff src/audlegacy/playlist.c @ 4811:7bf7f83a217e

rename src/audacious src/audlegacy so that both audlegacy and audacious can coexist.
author Yoshiki Yazawa <yaz@honeyplanet.jp>
date Wed, 26 Nov 2008 00:44:56 +0900
parents src/audacious/playlist.c@6284337e04fd
children 7ac9e8b91bbf
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/audlegacy/playlist.c	Wed Nov 26 00:44:56 2008 +0900
@@ -0,0 +1,3356 @@
+/*  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 "legacy/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
+
+const gchar *aud_titlestring_presets[] = {
+    "${title}",
+    "${?artist:${artist} - }${title}",
+    "${?artist:${artist} - }${?album:${album} - }${title}",
+    "${?artist:${artist} - }${?album:${album} - }${?track-number:${track-number}. }${title}",
+    "${?artist:${artist} }${?album:[ ${album} ] }${?artist:- }${?track-number:${track-number}. }${title}",
+    "${?album:${album} - }${title}"
+};
+const guint n_titlestring_presets = G_N_ELEMENTS(aud_titlestring_presets);
+
+
+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_assert_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;
+            return TRUE;
+        }
+    }
+
+    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_assert_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++);
+            playlist->tail = g_list_last(playlist->entries);
+        }
+
+        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_path_get_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;
+
+    playback_set_sample_params(rate, freq, nch);
+    event_queue_with_data_free("playlist info change", msg);
+
+    text = playlist_get_info_text(playlist);
+    event_queue_with_data_free("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();
+    else
+        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);
+        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_assert_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_assert_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->tail = g_list_last(playlist->entries);
+    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->tail = g_list_last(playlist->entries);
+    PLAYLIST_INCR_SERIAL(playlist);
+    PLAYLIST_UNLOCK(playlist);
+}
+
+void
+playlist_reverse(Playlist *playlist)
+{
+    PLAYLIST_LOCK(playlist);
+    playlist->entries = g_list_reverse(playlist->entries);
+    playlist->tail = g_list_last(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->tail = g_list_last(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;
+}
+
+const gchar *
+get_gentitle_format(void)
+{
+    guint titlestring_preset = cfg.titlestring_preset;
+
+    if (titlestring_preset < n_titlestring_presets)
+        return aud_titlestring_presets[titlestring_preset];
+
+    return cfg.gentitle_format;
+}