/*
 * Copyright (c) 2003, 2004 Jean-Yves Lefort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of Jean-Yves Lefort nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include <string.h>
#include <stdlib.h>
#include <glib/gi18n.h>
#include <libxml/parser.h>
#include "streamtuner.h"

/*** cpp *********************************************************************/

#define XIPH_HOME		"http://dir.xiph.org/"
#define XIPH_XML		"http://dir.xiph.org/yp.xml"

#define PARSER_STATE_IS_IN_DIRECTORY(state) \
  ((state)->tags && ! (state)->tags->next && ! strcmp((state)->tags->data, "directory"))

#define PARSE_ERROR		st_handler_notice(xiph_handler, _("parse error at %s"), G_STRLOC)

/*** types *******************************************************************/

typedef struct
{
  STStream	stream;
  
  char		*server_name;
  char		*listen_url;
  char		*server_type;
  char		*bitrate;
  int		channels;
  int		samplerate;
  char		*genre;
  char		*current_song;
} XiphStream;

enum {
  FIELD_SERVER_NAME,
  FIELD_LISTEN_URL,
  FIELD_SERVER_TYPE,
  FIELD_BITRATE,
  FIELD_CHANNELS,
  FIELD_SAMPLERATE,
  FIELD_GENRE,
  FIELD_CURRENT_SONG,
  FIELD_AUDIO		/* meta field (bitrate, channels and samplerate) */
};

enum {
  TAG_ROOT,
  TAG_DIRECTORY,
  TAG_ENTRY,
  TAG_SERVER_NAME,
  TAG_LISTEN_URL,
  TAG_SERVER_TYPE,
  TAG_BITRATE,
  TAG_CHANNELS,
  TAG_SAMPLERATE,
  TAG_GENRE,
  TAG_CURRENT_SONG,

  TAG_UNSUPPORTED
};

typedef struct
{
  GSList		*tags;
  GHashTable		*stream_properties;
  GList			*streams;
  char			*error;
} ParserState;
  
/*** variables ***************************************************************/

static STPlugin *xiph_plugin = NULL;
static STHandler *xiph_handler = NULL;

static struct
{
  char		*name;
  char		*label;
  char		*re;
  regex_t	compiled_re;
} stock_genres[] = {
  { "__alternative",	N_("Alternative"),	"alternative|indie|goth|college|industrial|punk|hardcore|ska" },
  { "__classical",	N_("Classical"),	"classical|opera|symphonic" },
  { "__country",	N_("Country"),		"country|swing" },
  { "__electronic",	N_("Electronic"),	"electronic|ambient|drum.*bass|trance|techno|house|downtempo|breakbeat|jungle|garage" },
  { "__rap",		N_("Hip-Hop/Rap"),	"hip ?hop|rap|turntabl|old school|new school" },
  { "__jazz",		N_("Jazz"),		"jazz|swing|big ?band" },
  { "__oldies",		N_("Oldies"),		"oldies|disco|50s|60s|70s|80s|90s" },
  { "__rock",		N_("Pop/Rock"),		"pop|rock|top ?40|metal" },
  { "__soul",		N_("R&B/Soul"),		"r ?(&|'? ?n ?'?) ?b|funk|soul|urban" },
  { "__spiritual",	N_("Spiritual"),	"spiritual|gospel|christian|muslim|jewish|religio" },
  { "__spoken",		N_("Spoken"),		"spoken|talk|comedy" },
  { "__world",		N_("World"),		"world|reggae|island|african|european|middle ?east|asia" },
  { "__other",		N_("Other"),		"various|mixed|misc|eclectic|film|show|instrumental" },
  { NULL }
};

static char *search_token = NULL;

/*** functions ***************************************************************/

static XiphStream *stream_new_cb (gpointer data);
static void stream_field_get_cb (XiphStream *stream,
				 STHandlerField *field,
				 GValue *value,
				 gpointer data);
static void stream_field_set_cb (XiphStream *stream,
				 STHandlerField *field,
				 const GValue *value,
				 gpointer data);
static void stream_stock_field_get_cb (XiphStream *stream,
				       STHandlerStockField stock_field,
				       GValue *value,
				       gpointer data);
static void stream_free_cb (XiphStream *stream, gpointer data);
static XiphStream *stream_copy (XiphStream *stream);

static char *stream_get_audio (XiphStream *stream);

static gboolean stream_tune_in_cb (XiphStream *stream,
				   gpointer data,
				   GError **err);
static gboolean stream_record_cb (XiphStream *stream,
				  gpointer data,
				  GError **err);

static GList *streams_match_genre (GList *streams, regex_t *regexp);
static GList *streams_match_any (GList *streams, const char *token);

static gboolean utf8_strcasecontains (const char *big, const char *little);

static gboolean reload_streams (GList **streams, GError **err);

static char *parser_state_get_stream_property_string (ParserState *state,
						      const char *name);
static int parser_state_get_stream_property_int (ParserState *state,
						 const char *name);

static xmlEntityPtr reload_streams_get_entity_cb (gpointer user_data,
						  const xmlChar *name);
static void reload_streams_start_element_cb (gpointer user_data,
					     const xmlChar *name,
					     const xmlChar **atts);
static void reload_streams_end_element_cb (gpointer user_data,
					   const xmlChar *name);
static void reload_streams_characters_cb (gpointer user_data,
					  const xmlChar *ch, int len);
static void reload_streams_warning_cb (gpointer user_data,
				       const char *format,
				       ...) G_GNUC_PRINTF(2, 3);
static void reload_streams_error_cb (gpointer user_data,
				     const char *format,
				     ...) G_GNUC_PRINTF(2, 3);

static gboolean search_url_cb (STCategory *category);

static gboolean check_api_version (GError **err);
static void init_handler (void);

/*** implementation **********************************************************/

static XiphStream *
stream_new_cb (gpointer data)
{
  return g_new0(XiphStream, 1);
}

static void
stream_field_get_cb (XiphStream *stream,
		     STHandlerField *field,
		     GValue *value,
		     gpointer data)
{
  switch (field->id)
    {
    case FIELD_SERVER_NAME:
      g_value_set_string(value, stream->server_name);
      break;
      
    case FIELD_LISTEN_URL:
      g_value_set_string(value, stream->listen_url);
      break;

    case FIELD_SERVER_TYPE:
      g_value_set_string(value, stream->server_type);
      break;

    case FIELD_BITRATE:
      g_value_set_string(value, stream->bitrate);
      break;

    case FIELD_CHANNELS:
      g_value_set_int(value, stream->channels);
      break;

    case FIELD_SAMPLERATE:
      g_value_set_int(value, stream->samplerate);
      break;

    case FIELD_GENRE:
      g_value_set_string(value, stream->genre);
      break;

    case FIELD_CURRENT_SONG:
      g_value_set_string(value, stream->current_song);
      break;

    case FIELD_AUDIO:
      g_value_take_string(value, stream_get_audio(stream));
      break;

    default:
      g_assert_not_reached();
    }
}

static void
stream_field_set_cb (XiphStream *stream,
		     STHandlerField *field,
		     const GValue *value,
		     gpointer data)
{
  switch (field->id)
    {
    case FIELD_SERVER_NAME:
      stream->server_name = g_value_dup_string(value);
      break;

    case FIELD_LISTEN_URL:
      stream->listen_url = g_value_dup_string(value);
      break;

    case FIELD_SERVER_TYPE:
      stream->server_type = g_value_dup_string(value);
      break;

    case FIELD_BITRATE:
      stream->bitrate = g_value_dup_string(value);
      break;

    case FIELD_CHANNELS:
      stream->channels = g_value_get_int(value);
      break;

    case FIELD_SAMPLERATE:
      stream->samplerate = g_value_get_int(value);
      break;

    case FIELD_GENRE:
      stream->genre = g_value_dup_string(value);
      break;

    case FIELD_CURRENT_SONG:
      stream->current_song = g_value_dup_string(value);
      break;

    default:
      g_assert_not_reached();
    }
}

static void
stream_stock_field_get_cb (XiphStream *stream,
			   STHandlerStockField stock_field,
			   GValue *value,
			   gpointer data)
{
  switch (stock_field)
    {
    case ST_HANDLER_STOCK_FIELD_NAME:
      g_value_set_string(value, stream->server_name);
      break;

    case ST_HANDLER_STOCK_FIELD_GENRE:
      g_value_set_string(value, stream->genre);
      break;

    case ST_HANDLER_STOCK_FIELD_DESCRIPTION:
    case ST_HANDLER_STOCK_FIELD_HOMEPAGE:
      /* nop */
      break;

    case ST_HANDLER_STOCK_FIELD_URI_LIST:
      {
	GValueArray *value_array;
	GValue uri_value = { 0, };

	value_array = g_value_array_new(1);

	g_value_init(&uri_value, G_TYPE_STRING);
	g_value_set_string(&uri_value, stream->listen_url);
	g_value_array_append(value_array, &uri_value);
	g_value_unset(&uri_value);

	g_value_take_boxed(value, value_array);

	break;
      }
    }
}

static void
stream_free_cb (XiphStream *stream, gpointer data)
{
  g_free(stream->server_name);
  g_free(stream->listen_url);
  g_free(stream->server_type);
  g_free(stream->bitrate);
  g_free(stream->genre);
  g_free(stream->current_song);

  st_stream_free((STStream *) stream);
}

static XiphStream *
stream_copy (XiphStream *stream)
{
  XiphStream *copy;

  copy = stream_new_cb(NULL);
  ((STStream *) copy)->name = g_strdup(((STStream *) stream)->name);
  copy->server_name = g_strdup(stream->server_name);
  copy->listen_url = g_strdup(stream->listen_url);
  copy->server_type = g_strdup(stream->server_type);
  copy->bitrate = g_strdup(stream->bitrate);
  copy->channels = stream->channels;
  copy->samplerate = stream->samplerate;
  copy->genre = g_strdup(stream->genre);
  copy->current_song = g_strdup(stream->current_song);

  return copy;
}

static char *
stream_get_audio (XiphStream *stream)
{
  GString *audio;
  char *str;

  g_return_val_if_fail(stream != NULL, NULL);

  audio = g_string_new(NULL);

  if (stream->bitrate)
    {
      if (g_str_has_prefix(stream->bitrate, "Quality"))
	g_string_append(audio, stream->bitrate);
      else if (st_str_like(stream->bitrate, ST_NUMERIC))
	{
	  int bitrate = atoi(stream->bitrate);

	  if (bitrate > 0 && bitrate < 1000000) /* avoid bogus bitrates */
	    {
	      /*
	       * Some bitrates are given in bps. To properly convert
	       * bps to kbps, we consider that if the bitrate is
	       * superior to 1000, the unit is bps.
	       *
	       * Also, bitrates such as "16000" probably mean
	       * "16kbps", so we use a kilo of 1000, not 1024.
	       */
	      if (bitrate > 1000)
		bitrate /= 1000;

	      str = st_format_bitrate(bitrate);
	      g_string_append(audio, str);
	      g_free(str);
	    }
	}
    }

  if (stream->samplerate > 0)
    {
      if (*audio->str)
	g_string_append(audio, ", ");

      str = st_format_samplerate(stream->samplerate);
      g_string_append(audio, str);
      g_free(str);
    }

  if (stream->channels > 0)
    {
      if (*audio->str)
	g_string_append(audio, ", ");

      str = st_format_channels(stream->channels);
      g_string_append(audio, str);
      g_free(str);
    }
  
  if (*audio->str)
    return g_string_free(audio, FALSE);
  else
    {
      g_string_free(audio, TRUE);
      return NULL;
    }
}

static gboolean
stream_tune_in_cb (XiphStream *stream,
		   gpointer data,
		   GError **err)
{
  return st_action_run("play-stream", stream->listen_url, err);
}

static gboolean
stream_record_cb (XiphStream *stream,
		  gpointer data,
		  GError **err)
{
  return st_action_run("record-stream", stream->listen_url, err);
}

static GList *
streams_match_genre (GList *streams, regex_t *regexp)
{
  GList *matching = NULL;
  GList *l;

  for (l = streams; l; l = l->next)
    {
      XiphStream *stream = l->data;

      if (st_re_match(regexp, stream->genre))
	matching = g_list_append(matching, stream_copy(stream));
    }

  return matching;
}

static GList *
streams_match_any (GList *streams, const char *token)
{
  GList *matching = NULL;
  GList *l;

  for (l = streams; l; l = l->next)
    {
      XiphStream *stream = l->data;

      if (utf8_strcasecontains(stream->server_name, token)
	  || utf8_strcasecontains(stream->listen_url, token)
	  || utf8_strcasecontains(stream->server_type, token)
	  || utf8_strcasecontains(stream->genre, token)
	  || utf8_strcasecontains(stream->current_song, token))
	matching = g_list_append(matching, stream_copy(stream));
    }

  return matching;
}

static gboolean
utf8_strcasecontains (const char *big, const char *little)
{
  gboolean contains;
  char *normalized_big;
  char *normalized_little;
  char *case_normalized_big;
  char *case_normalized_little;

  g_return_val_if_fail(big != NULL, FALSE);
  g_return_val_if_fail(little != NULL, FALSE);

  normalized_big = g_utf8_normalize(big, -1, G_NORMALIZE_ALL);
  normalized_little = g_utf8_normalize(little, -1, G_NORMALIZE_ALL);
  case_normalized_big = g_utf8_casefold(normalized_big, -1);
  case_normalized_little = g_utf8_casefold(normalized_little, -1);

  contains = strstr(case_normalized_big, case_normalized_little) != NULL;

  g_free(normalized_big);
  g_free(normalized_little);
  g_free(case_normalized_big);
  g_free(case_normalized_little);

  return contains;
}

static gboolean
reload_multiple_cb (GNode **categories,
		    GHashTable **streams,
		    gpointer data,
		    GError **err)
{
  GList *streams_list = NULL;
  int i;

  if (! reload_streams(&streams_list, err))
    return FALSE;

  *streams = g_hash_table_new(g_str_hash, g_str_equal);

  g_hash_table_insert(*streams, "__main", streams_list);

  if (search_token)
    g_hash_table_insert(*streams,
			"__search",
			streams_match_any(streams_list, search_token));
  
  for (i = 0; stock_genres[i].name; i++)
    g_hash_table_insert(*streams,
			stock_genres[i].name,
			streams_match_genre(streams_list, &stock_genres[i].compiled_re));

  return TRUE;
}

static gboolean
reload_streams (GList **streams, GError **err)
{
  gboolean status;
  STTransferSession *session;
  char *body;
  xmlSAXHandler sax_handler = { NULL };
  ParserState state;

  session = st_transfer_session_new();
  status = st_transfer_session_get(session, XIPH_XML, 0, NULL, &body, err);
  st_transfer_session_free(session);

  if (! status)
    return FALSE;

  sax_handler.getEntity = reload_streams_get_entity_cb;
  sax_handler.startElement = reload_streams_start_element_cb;
  sax_handler.endElement = reload_streams_end_element_cb;
  sax_handler.characters = reload_streams_characters_cb;
  sax_handler.warning = reload_streams_warning_cb;
  sax_handler.error = reload_streams_error_cb;
  sax_handler.fatalError = reload_streams_error_cb;

  state.tags = NULL;
  state.stream_properties = NULL;
  state.streams = NULL;
  state.error = NULL;

  status = xmlSAXUserParseMemory(&sax_handler, &state, body, strlen(body)) == 0;
  g_free(body);

  g_slist_foreach(state.tags, (GFunc) g_free, NULL);
  g_slist_free(state.tags);

  if (state.stream_properties)
    {
      g_hash_table_destroy(state.stream_properties);
      if (status) /* only display warning if the parsing was successful */
	PARSE_ERROR;
    }

  if (status)
    *streams = state.streams;
  else
    {
      g_list_foreach(state.streams, (GFunc) stream_free_cb, NULL);
      g_list_free(state.streams);
      g_set_error(err, 0, 0, _("unable to parse XML document: %s"), state.error ? state.error : _("unknown error"));
    }

  g_free(state.error);

  return status;
}

static char *
parser_state_get_stream_property_string (ParserState *state, const char *name)
{
  char *str;

  g_return_val_if_fail(state != NULL, NULL);
  g_return_val_if_fail(state->stream_properties != NULL, NULL);

  str =  g_strdup(g_hash_table_lookup(state->stream_properties, name));
  if (str)
    {
      int i;

      /* remove trailing \r and \n */
      for (i = strlen(str) - 1; i >= 0; i--)
	if (str[i] == '\r' || str[i] == '\n')
	  str[i] = 0;
	else
	  break;

      /* replace \r and \n with a space character */
      for (i = 0; str[i]; i++)
	if (str[i] == '\r' || str[i] == '\n')
	  str[i] = ' ';
    }

  return str;
}

static int
parser_state_get_stream_property_int (ParserState *state, const char *name)
{
  const char *str;

  g_return_val_if_fail(state != NULL, 0);
  g_return_val_if_fail(state->stream_properties != NULL, 0);

  str = g_hash_table_lookup(state->stream_properties, name);
  return str ? atoi(str) : 0;
}

static xmlEntityPtr
reload_streams_get_entity_cb (gpointer user_data, const xmlChar *name)
{
  return xmlGetPredefinedEntity(name);
}

static void
reload_streams_start_element_cb (gpointer user_data,
				 const xmlChar *name,
				 const xmlChar **atts)
{
  ParserState *state = user_data;

  if (PARSER_STATE_IS_IN_DIRECTORY(state) && ! strcmp(name, "entry"))
    {
      if (state->stream_properties)
	{
	  PARSE_ERROR;
	  g_hash_table_destroy(state->stream_properties);
	}
      state->stream_properties = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    }

  state->tags = g_slist_prepend(state->tags, g_strdup(name));
}

static void
reload_streams_end_element_cb (gpointer user_data, const xmlChar *name)
{
  ParserState *state = user_data;
  char *current_tag;

  current_tag = state->tags ? state->tags->data : NULL;
  if (current_tag && ! strcmp(current_tag, name))
    {
      g_free(current_tag);
      state->tags = g_slist_delete_link(state->tags, state->tags);
    }
  else
    PARSE_ERROR;

  if (PARSER_STATE_IS_IN_DIRECTORY(state) && ! strcmp(name, "entry"))
    {
      char *listen_url;

      listen_url = parser_state_get_stream_property_string(state, "listen_url");
      if (listen_url)
	{
	  XiphStream *stream;

	  stream = stream_new_cb(NULL);
	  
	  stream->server_name = parser_state_get_stream_property_string(state, "server_name");
	  stream->listen_url = listen_url;
	  stream->server_type = parser_state_get_stream_property_string(state, "server_type");
	  stream->bitrate = parser_state_get_stream_property_string(state, "bitrate");
	  stream->channels = parser_state_get_stream_property_int(state, "channels");
	  stream->samplerate = parser_state_get_stream_property_int(state, "samplerate");
	  stream->genre = parser_state_get_stream_property_string(state, "genre");
	  stream->current_song = parser_state_get_stream_property_string(state, "current_song");

	  ((STStream *) stream)->name = g_strdup(stream->listen_url);
	  state->streams = g_list_append(state->streams, stream);
	}
      else
	PARSE_ERROR;

      g_hash_table_destroy(state->stream_properties);
      state->stream_properties = NULL;
    }
}

static void
reload_streams_characters_cb (gpointer user_data, const xmlChar *ch, int len)
{
  ParserState *state = user_data;

  if (state->stream_properties)
    {
      const char *current_tag;
      char *value;
      const char *str;
      char *new_str;

      g_return_if_fail(state->tags != NULL);
      current_tag = state->tags->data;

      value = g_strndup(ch, len);

      str = g_hash_table_lookup(state->stream_properties, current_tag);
      if (str)
	{
	  new_str = g_strconcat(str, value, NULL);
	  g_free(value);
	}
      else
	new_str = value;

      g_hash_table_insert(state->stream_properties, g_strdup(current_tag), new_str);
    }
}

static void
reload_streams_warning_cb (gpointer user_data, const char *format, ...)
{
  va_list args;
  char *message;

  va_start(args, format);
  message = g_strdup_vprintf(format, args);
  va_end(args);

  st_handler_notice(xiph_handler, _("XML document: %s"), message);
  g_free(message);
}

static void
reload_streams_error_cb (gpointer user_data, const char *format, ...)
{
  ParserState *state = user_data;
  va_list args;
  char *message;

  va_start(args, format);
  message = g_strdup_vprintf(format, args);
  va_end(args);

  if (! state->error)		/* only keep the first error for the UI... */
    state->error = g_strdup(message);

  /* ...and display them all */
  st_handler_notice(xiph_handler, _("XML document: unrecoverable error: %s"), message);
  g_free(message);
}

static gboolean
search_url_cb (STCategory *category)
{
  char *str;

  str = st_search_dialog();
  if (str)
    {
      g_free(category->label);
      category->label = g_strdup_printf(_("Search results for \"%s\""), str);

      g_free(search_token);
      search_token = str;

      return TRUE;
    }
  else
    return FALSE;
}

static void
init_handler (void)
{
  GNode *stock_categories;
  STCategory *category;
  int i;
  STHandlerField *field;

  xiph_handler = st_handler_new_from_plugin(xiph_plugin);

  st_handler_set_description(xiph_handler, _("Xiph.org Streaming Directory"));
  st_handler_set_home(xiph_handler, XIPH_HOME);

  stock_categories = g_node_new(NULL);

  category = st_category_new();
  category->name = "__main";
  category->label = _("All");
  
  g_node_append_data(stock_categories, category);

  category = st_category_new();
  category->name = "__search";
  category->label = g_strdup(_("Search"));
  category->url_cb = search_url_cb;

  g_node_append_data(stock_categories, category);

  for (i = 0; stock_genres[i].name; i++)
    {
      int status;

      /* compile the regexp */
      status = regcomp(&stock_genres[i].compiled_re, stock_genres[i].re, REG_EXTENDED | REG_ICASE);
      g_return_if_fail(status == 0);

      category = st_category_new();
      category->name = stock_genres[i].name;
      category->label = _(stock_genres[i].label);

      g_node_append_data(stock_categories, category);
    }
  
  st_handler_set_stock_categories(xiph_handler, stock_categories);

  st_handler_bind(xiph_handler, ST_HANDLER_EVENT_RELOAD_MULTIPLE, reload_multiple_cb, NULL);

  st_handler_bind(xiph_handler, ST_HANDLER_EVENT_STREAM_NEW, stream_new_cb, NULL);
  st_handler_bind(xiph_handler, ST_HANDLER_EVENT_STREAM_FIELD_GET, stream_field_get_cb, NULL);
  st_handler_bind(xiph_handler, ST_HANDLER_EVENT_STREAM_FIELD_SET, stream_field_set_cb, NULL);
  st_handler_bind(xiph_handler, ST_HANDLER_EVENT_STREAM_STOCK_FIELD_GET, stream_stock_field_get_cb, NULL);
  st_handler_bind(xiph_handler, ST_HANDLER_EVENT_STREAM_FREE, stream_free_cb, NULL);

  st_handler_bind(xiph_handler, ST_HANDLER_EVENT_STREAM_TUNE_IN, stream_tune_in_cb, NULL);
  st_handler_bind(xiph_handler, ST_HANDLER_EVENT_STREAM_RECORD, stream_record_cb, NULL);

  /* visible fields */

  field = st_handler_field_new(FIELD_SERVER_NAME,
			       _("Name"),
			       G_TYPE_STRING,
			       ST_HANDLER_FIELD_VISIBLE);
  st_handler_field_set_description(field, _("The stream name"));
  st_handler_add_field(xiph_handler, field);

  field = st_handler_field_new(FIELD_GENRE,
			       _("Genre"),
			       G_TYPE_STRING,
			       ST_HANDLER_FIELD_VISIBLE);
  st_handler_field_set_description(field, _("The stream genre"));
  st_handler_add_field(xiph_handler, field);

  field = st_handler_field_new(FIELD_CURRENT_SONG,
			       _("Current song"),
			       G_TYPE_STRING,
			       ST_HANDLER_FIELD_VISIBLE);
  st_handler_field_set_description(field, _("The currently playing song"));
  st_handler_add_field(xiph_handler, field);

  field = st_handler_field_new(FIELD_SERVER_TYPE,
			       _("Type"),
			       G_TYPE_STRING,
			       ST_HANDLER_FIELD_VISIBLE);
  st_handler_field_set_description(field, _("The stream type"));
  st_handler_add_field(xiph_handler, field);

  field = st_handler_field_new(FIELD_AUDIO,
			       _("Audio"),
			       G_TYPE_STRING,
			       ST_HANDLER_FIELD_VISIBLE
			       | ST_HANDLER_FIELD_VOLATILE);
  st_handler_field_set_description(field, _("The stream audio properties"));
  st_handler_add_field(xiph_handler, field);

  field = st_handler_field_new(FIELD_LISTEN_URL,
			       _("URL"),
			       G_TYPE_STRING,
			       ST_HANDLER_FIELD_VISIBLE
			       | ST_HANDLER_FIELD_START_HIDDEN);
  st_handler_field_set_description(field, _("The stream listen URL"));
  st_handler_add_field(xiph_handler, field);
  
  /* invisible fields */

  st_handler_add_field(xiph_handler, st_handler_field_new(FIELD_BITRATE,
							  _("Bitrate"),
							  G_TYPE_STRING,
							  0));
  st_handler_add_field(xiph_handler, st_handler_field_new(FIELD_CHANNELS,
							  _("Channels"),
							  G_TYPE_INT,
							  0));
  st_handler_add_field(xiph_handler, st_handler_field_new(FIELD_SAMPLERATE,
							  _("Sample rate"),
							  G_TYPE_INT,
							  0));

  st_handlers_add(xiph_handler);
}

static gboolean
check_api_version (GError **err)
{
  if (st_check_api_version(5, 8))
    return TRUE;
  else
    {
      g_set_error(err, 0, 0, _("API version mismatch"));
      return FALSE;
    }
}

G_MODULE_EXPORT gboolean
plugin_get_info (STPlugin *plugin, GError **err)
{
  GdkPixbuf *pixbuf;

  if (! check_api_version(err))
    return FALSE;

  xiph_plugin = plugin;

  st_plugin_set_name(plugin, "xiph");
  st_plugin_set_label(plugin, "Xiph");

  pixbuf = st_pixbuf_new_from_file(UIDIR "/xiph.png");
  if (pixbuf)
    {
      st_plugin_set_icon_from_pixbuf(plugin, pixbuf);
      g_object_unref(pixbuf);
    }

  return TRUE;
}

G_MODULE_EXPORT gboolean
plugin_init (GError **err)
{
  if (! check_api_version(err))
    return FALSE;

  xmlInitParser();

  init_handler();

  st_action_register("record-stream", _("Record a stream"), "xterm -e streamripper %q");
  st_action_register("play-stream", _("Listen to a stream"), "xmms %q");

  return TRUE;
}
