/*
 * Copyright (c) 2002, 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 "streamtuner.h"

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

#define LIVE365_ROOT		"http://www.live365.com/"
#define DIRECTORY_PREFIX	"cgi-bin/directory.cgi?genre="

#define MAX_STREAMS_PER_PAGE	200 /* enforced by Live365 */

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

#define CONFIG_USE_MEMBERSHIP		"use-membership"
#define CONFIG_NAME			"name"
#define CONFIG_PASSWORD			"password"
#define CONFIG_STREAM_LIMIT_ENABLED	"stream-limit-enabled"
#define CONFIG_STREAM_LIMIT		"stream-limit"

#define MIN_STREAM_LIMIT	0
#define MAX_STREAM_LIMIT	9999

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

typedef enum
{
  ACCESS_ALL,
  ACCESS_VIP,
  ACCESS_SUBSCRIPTION
} Access;

typedef struct
{
  STStream	stream;
  
  char		*title;
  char		*genre;
  char		*description;
  char		*broadcaster;
  char		*audio;
  char		*homepage;
  int		station_id;
  Access	access;
  int		tlh;
  double	rating;
} Live365Stream;

typedef struct
{
  char		*sane_id;
  char		*session_id;
} LoginInfo;

typedef struct
{
  GNode		**categories;
  GNode		*parent;
} ReloadCategoriesInfo;

typedef struct
{
  GList			**streams;
  Live365Stream		*stream;
  Live365Stream		*finished_stream;
  gboolean		has_next_page;
} ReloadStreamsInfo;

enum {
  FIELD_TITLE,
  FIELD_GENRE,
  FIELD_DESCRIPTION,
  FIELD_BROADCASTER,
  FIELD_AUDIO,
  FIELD_HOMEPAGE,
  FIELD_STATION_ID,
  FIELD_URL,
  FIELD_ACCESS,
  FIELD_ACCESS_STRING,
  FIELD_TLH,
  FIELD_RATING,
  FIELD_RATING_STRING
};

/*** variables ***************************************************************/

static STPlugin *live365_plugin = NULL;
static STHandler *live365_handler = NULL;

static GNode *session_categories = NULL;

static GtkWidget *preferences_stream_limit_check;
static GtkWidget *preferences_stream_limit_spin;
static GtkWidget *preferences_stream_limit_label;
static GtkWidget *preferences_use_membership_check;
static GtkWidget *preferences_name_label;
static GtkWidget *preferences_name_entry;
static GtkWidget *preferences_password_label;
static GtkWidget *preferences_password_entry;

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

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

static char		*stream_get_url		(Live365Stream	*stream,
						 gboolean	blocking);

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

static char		*get_session_params	(gboolean	blocking);
static gboolean		login			(const char	*name,
						 const char	*password,
						 char		**sane_id,
						 char		**session_id,
						 GError		**err);
static void		login_header_cb		(const char	*line,
						 gpointer	data);

static gboolean		reload_cb		(STCategory	*category,
						 GNode		**categories,
						 GList		**streams,
						 gpointer	data,
						 GError		**err);

static GNode *		categories_copy		(void);
static gboolean		categories_copy_cb	(GNode		*node,
						 gpointer	data);
static gboolean		reload_categories	(GNode		**categories,
						 GError		**err);
static void		reload_categories_body_cb (const char	*line,
						   gpointer	data);

static gboolean		reload_streams		(STCategory	*category,
						 GList		**streams,
						 GError		**err);
static void		reload_streams_body_cb	(const char	*line,
						 gpointer	data);

static gboolean		search_url_cb		(STCategory	*category);

static GtkWidget *	preferences_widget_new_cb	(gpointer        data);
static void		preferences_update_sensitivity	(void);
static void		preferences_stream_limit_toggled_h        (GtkToggleButton *button,
								   gpointer        user_data);
static void		preferences_stream_limit_changed_h        (GtkSpinButton   *spin,
								   gpointer        user_data);
static void		preferences_use_membership_toggled_h      (GtkToggleButton *button,
								   gpointer        user_data);
static void		preferences_credentials_activate_h        (GtkEntry        *entry,
								   gpointer        user_data);
static gboolean		preferences_credentials_focus_out_event_h (GtkWidget       *widget,
								   GdkEventFocus   *event,
								   gpointer        user_data);

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

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

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

static void
stream_field_get_cb (Live365Stream *stream,
		     STHandlerField *field,
		     GValue *value,
		     gpointer data)
{
  switch (field->id)
    {
    case FIELD_TITLE:
      g_value_set_string(value, stream->title);
      break;

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

    case FIELD_DESCRIPTION:
      g_value_set_string(value, stream->description);
      break;

    case FIELD_BROADCASTER:
      g_value_set_string(value, stream->broadcaster);
      break;

    case FIELD_AUDIO: 
      g_value_set_string(value, stream->audio);
      break;

    case FIELD_HOMEPAGE:
      g_value_set_string(value, stream->homepage);
      break;

    case FIELD_STATION_ID:
      g_value_set_int(value, stream->station_id);
      break;

    case FIELD_URL:
      g_value_take_string(value, stream_get_url(stream, FALSE));
      break;

    case FIELD_ACCESS:
      g_value_set_int(value, stream->access);
      break;

    case FIELD_ACCESS_STRING:
      {
	const char *str = NULL;

	switch (stream->access)
	  {
	  case ACCESS_ALL:		str = _("All"); break;
	  case ACCESS_VIP:		str = _("VIP"); break;
	  case ACCESS_SUBSCRIPTION:	str = _("Subscription"); break;
	  }

	g_value_set_string(value, str);
	break;
      }

    case FIELD_TLH:
      g_value_set_int(value, stream->tlh);
      break;

    case FIELD_RATING:
      g_value_set_double(value, stream->rating);
      break;

    case FIELD_RATING_STRING:
      g_value_take_string(value, g_strdup_printf("%.2f", stream->rating));
      break;
      
    default:
      g_assert_not_reached();
    }
}

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

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

    case FIELD_DESCRIPTION: 
      stream->description = g_value_dup_string(value);
      break;

    case FIELD_BROADCASTER:
      stream->broadcaster = g_value_dup_string(value);
      break;

    case FIELD_AUDIO: 
      stream->audio = g_value_dup_string(value);
      break;

    case FIELD_HOMEPAGE:
      stream->homepage = g_value_dup_string(value);
      break;

    case FIELD_STATION_ID:
      stream->station_id = g_value_get_int(value);
      break;

    case FIELD_ACCESS:
      stream->access = g_value_get_int(value);
      break;

    case FIELD_TLH:
      stream->tlh = g_value_get_int(value);
      break;

    case FIELD_RATING:
      stream->rating = g_value_get_double(value);
      break;

    default:
      g_assert_not_reached();
    }
}

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

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

    case ST_HANDLER_STOCK_FIELD_DESCRIPTION:
      g_value_set_string(value, stream->description);
      break;

    case ST_HANDLER_STOCK_FIELD_HOMEPAGE:
      g_value_set_string(value, stream->homepage);
      break;

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

	value_array = g_value_array_new(1);

	uri = stream_get_url(stream, FALSE);
	g_value_init(&uri_value, G_TYPE_STRING);
	g_value_take_string(&uri_value, uri);
	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 (Live365Stream *stream, gpointer data)
{
  g_free(stream->title);
  g_free(stream->genre);
  g_free(stream->description);
  g_free(stream->broadcaster);
  g_free(stream->audio);
  g_free(stream->homepage);

  st_stream_free((STStream *) stream);
}

static char *
stream_get_url (Live365Stream *stream, gboolean blocking)
{
  char *params;
  char *url;

  g_return_val_if_fail(stream != NULL, NULL);

  params = get_session_params(blocking);
  if (params)
    {
      url = g_strdup_printf(LIVE365_ROOT "play/%i%s", stream->station_id, params);
      g_free(params);
    }
  else
    url = g_strdup_printf(LIVE365_ROOT "play/%i", stream->station_id);

  return url;
}

static gboolean
stream_tune_in_cb (Live365Stream *stream, gpointer data, GError **err)
{
  char *url;
  gboolean status;

  url = stream_get_url(stream, TRUE);
  status = st_action_run("play-stream", url, err);
  g_free(url);

  return status;
}

static gboolean
stream_record_cb (Live365Stream *stream, gpointer data, GError **err)
{
  char *url;
  gboolean status;

  url = stream_get_url(stream, TRUE);
  status = st_action_run("record-stream", url, err);
  g_free(url);

  return status;
}

static gboolean
stream_browse_cb (Live365Stream *stream, gpointer data, GError **err)
{
  return st_action_run("view-web", stream->homepage, err);
}

static char *
get_session_params (gboolean blocking)
{
  char *config_name;
  char *config_password;
  char *name = NULL;
  char *password = NULL;
  char *params = NULL;
  
  if (! st_handler_config_get_boolean(live365_handler, CONFIG_USE_MEMBERSHIP))
    return NULL;

  config_name = st_handler_config_get_string(live365_handler, CONFIG_NAME);
  config_password = st_handler_config_get_string(live365_handler, CONFIG_PASSWORD);

  if (config_name && config_password)
    {
      name = g_strdup(config_name);
      password = g_strdup(config_password);
    }
  else if (blocking)
    st_auth_dialog(config_name,
		   config_password,
		   &name,
		   &password,
		   _("Password required"),
		   _("Enter your Live365 account name and password."));

  g_free(config_name);
  g_free(config_password);
  
  if (name && password)
    {
      static char *global_name = NULL;
      static char *global_password = NULL;
      static char *global_params = NULL;
      G_LOCK_DEFINE_STATIC(global);

      G_LOCK(global);
      if (global_params
	  && global_name && ! strcmp(global_name, name)
	  && global_password && ! strcmp(global_password, password))
	params = g_strdup(global_params);
      G_UNLOCK(global);

      if (! params && blocking)
	{
	  char *sane_id;
	  char *session_id;
	  GError *err = NULL;
	  
	  G_LOCK(global);
	  g_free(global_name);
	  global_name = g_strdup(name);
	  g_free(global_password);
	  global_password = g_strdup(password);
	  g_free(global_params);
	  global_params = NULL;
	  G_UNLOCK(global);
	  
	  if (login(name, password, &sane_id, &session_id, &err))
	    {
	      G_LOCK(global);
	      global_params = g_strdup_printf("?SaneID=%s&membername=%s&session=%s", sane_id, name, session_id);
	      g_free(sane_id);
	      g_free(session_id);
	      params = g_strdup(global_params);
	      G_UNLOCK(global);
	    }
	  else
	    {
	      char *normalized;
	      
	      normalized = st_dialog_normalize(err->message);
	      g_error_free(err);
	      
	      st_error_dialog(_("Unable to log into Live365"), "%s", normalized);
	      g_free(normalized);
	    }
	}
    }
  
  g_free(name);
  g_free(password);

  return params;
}

static gboolean
login (const char *name,
       const char *password,
       char **sane_id,
       char **session_id,
       GError **err)
{
  char *escaped_name;
  char *escaped_password;
  char *url;
  STTransferSession *session;
  gboolean status;
  LoginInfo info;

  g_return_val_if_fail(name != NULL, FALSE);
  g_return_val_if_fail(password != NULL, FALSE);
  g_return_val_if_fail(sane_id != NULL, FALSE);
  g_return_val_if_fail(session_id != NULL, FALSE);

  escaped_name = st_transfer_escape(name);
  escaped_password = st_transfer_escape(password);
  url = g_strdup_printf(LIVE365_ROOT "cgi-bin/login.cgi?url=http://www.live365.com/index.live&membername=%s&password=%s", escaped_name, escaped_password);
  g_free(escaped_name);
  g_free(escaped_password);
      
  info.sane_id = NULL;
  info.session_id = NULL;

  session = st_transfer_session_new();
  status = st_transfer_session_get_by_line(session,
					   url,
					   0,
					   login_header_cb,
					   &info,
					   NULL,
					   NULL,
					   err);
  st_transfer_session_free(session);

  if (status)
    {
      if (info.sane_id && info.session_id)
	{
	  *sane_id = info.sane_id;
	  *session_id = info.session_id;
	}
      else
	{
	  g_free(info.sane_id);
	  g_free(info.session_id);
	  g_set_error(err, 0, 0, _("invalid username or password"));
	  status = FALSE;
	}
    }

  return status;
}

static void
login_header_cb (const char *line, gpointer data)
{
  LoginInfo *info = data;
  char *s1, *s2, *s3;

  if (! info->sane_id
      && (s1 = st_str_has_prefix_span(line, "Set-Cookie: SaneID="))
      && (s2 = strchr(s1, ';')))
    info->sane_id = g_strndup(s1, s2 - s1);
  else if (! info->session_id
	   && (s1 = st_str_has_prefix_span(line, "Set-Cookie: sessionid="))
	   && (s2 = st_strstr_span(s1, "%3A"))
	   && (s3 = strchr(s2, ';')))
    info->session_id = g_strndup(s2, s3 - s2);
}

static gboolean
reload_cb (STCategory *category,
	   GNode **categories,
	   GList **streams,
	   gpointer data,
	   GError **err)
{
  if (! session_categories)
    {
      if (! reload_categories(&session_categories, err))
	return FALSE;
    }
  
  *categories = categories_copy();

  if (st_is_aborted())
    return FALSE;

  return category->url_postfix
    ? reload_streams(category, streams, err)
    : TRUE;
}

static GNode *
categories_copy (void)
{
  GNode *node;

  node = g_node_copy(session_categories);
  g_node_traverse(node, G_IN_ORDER, G_TRAVERSE_ALL, -1, categories_copy_cb, NULL);

  return node;
}

static gboolean
categories_copy_cb (GNode *node, gpointer data)
{
  STCategory *category = node->data;

  if (category)
    {
      STCategory *copy;

      copy = st_category_new();
      copy->name = g_strdup(category->name);
      copy->label = g_strdup(category->label);
      copy->url_postfix = g_strdup(category->url_postfix);

      node->data = copy;
    }

  return FALSE;
}

static gboolean
reload_categories (GNode **categories, GError **err)
{
  ReloadCategoriesInfo info;
  STTransferSession *session;
  gboolean status;

  *categories = g_node_new(NULL);

  info.categories = categories;
  info.parent = NULL;

  session = st_transfer_session_new();
  status = st_transfer_session_get_by_line(session,
					   LIVE365_ROOT "listen/",
					   ST_TRANSFER_UTF8 | ST_TRANSFER_PARSE_HTTP_CHARSET,
					   NULL,
					   NULL,
					   reload_categories_body_cb,
					   &info,
					   err);
  st_transfer_session_free(session);

  return status;
}

static void
reload_categories_body_cb (const char *line, gpointer data)
{
  ReloadCategoriesInfo *info = data;
  STCategory *category;
  char *s1, *s2, *s3, *s4, *s5;

  if ((s1 = st_strstr_span(line, "<a href=\"/cgi-bin/directory"))
      && (s2 = st_strstr_span(s1, "&genre="))
      && (s3 = strchr(s2, '"'))
      && (s4 = st_strchr_span(s3, '>'))
      && (s5 = strstr(s4, "</a>")))
    {
      category = st_category_new();
      category->name = g_strndup(s2, s3 - s2);
      category->label = st_sgml_ref_expand_len(s4, s5 - s4);
      category->url_postfix = g_strconcat(DIRECTORY_PREFIX, category->name, NULL);
    }
  else if ((s1 = st_strstr_span(line, "<a href=\"/genres/"))
	   && (s2 = strchr(s1, '/'))
	   && (s3 = st_strchr_span(s2, '>'))
	   && (s4 = strstr(s3, "</a>")))
    {
      category = st_category_new();
      category->name = g_strndup(s1, s2 - s1);
      category->label = st_sgml_ref_expand_len(s3, s4 - s3);
    }
  else
    return;

  if (g_str_has_prefix(line, "\t\t\t-"))
    {
      if (info->parent)
	g_node_append_data(info->parent, category);
      else
	{
	  PARSE_ERROR;
	  st_category_free(category);
	}
    }
  else
    info->parent = g_node_append_data(*info->categories, category);
}

static gboolean
reload_streams (STCategory *category, GList **streams, GError **err)
{
  STTransferSession *session;
  ReloadStreamsInfo info;
  gboolean status;
  int stream_limit;
  int requested_streams = 0;
  int received_streams = 0;

  *streams = NULL;
  info.streams = streams;

  session = st_transfer_session_new();

  stream_limit = st_handler_config_get_boolean(live365_handler, CONFIG_STREAM_LIMIT_ENABLED)
    ? st_handler_config_get_int(live365_handler, CONFIG_STREAM_LIMIT)
    : -1;

  do
    {
      char *url;
      int rows;
      
      if (requested_streams != 0 && st_is_aborted())
	{
	  status = FALSE;
	  break;
	}
      
      rows = stream_limit == -1
	? MAX_STREAMS_PER_PAGE
	: MIN(stream_limit - received_streams, MAX_STREAMS_PER_PAGE);

      url = g_strdup_printf(LIVE365_ROOT "%s&rows=%i&first=%i", category->url_postfix, rows, requested_streams + 1);

      requested_streams += rows;

      info.stream = NULL;
      info.has_next_page = FALSE;

      status = st_transfer_session_get_by_line(session,
					       url,
					       ST_TRANSFER_UTF8 | ST_TRANSFER_PARSE_HTTP_CHARSET | ST_TRANSFER_PARSE_HTML_CHARSET,
					       NULL,
					       NULL,
					       reload_streams_body_cb,
					       &info,
					       err);
      g_free(url);

      received_streams = g_list_length(*streams);
      
      if (info.stream)
	{
	  stream_free_cb(info.stream, NULL);
	  if (status) /* only display warning if the transfer was otherwise correct */
	    PARSE_ERROR;
	}
    }
  while (status && info.has_next_page
	 && (stream_limit == -1 || received_streams < stream_limit));

  st_transfer_session_free(session);

  return status;
}

static void
reload_streams_body_cb (const char *line, gpointer data)
{
  ReloadStreamsInfo *info = data;
  char *s1, *s2, *s3, *s4, *s5, *s6, *s7;
  char *word1, *word2, *word3;

  if ((s1 = st_str_has_prefix_span(line, "<TD  CLASS=\"icon\""))
      && (s2 = st_strstr_span(s1, "DrawPlayIcon")))
    {
      if (info->stream)	/* a malformed stream remains, free it */
	{
	  PARSE_ERROR;
	  stream_free_cb(info->stream, NULL);
	}
      
      info->stream = stream_new_cb(NULL);
      if (strstr(s2, "'OK'"))
	info->stream->access = ACCESS_ALL;
      else if (strstr(s2, "'PM_ONLY'"))
	info->stream->access = ACCESS_VIP;
      else if (strstr(s2, "'SUBSCRIPTION'"))
	info->stream->access = ACCESS_SUBSCRIPTION;
      else
	PARSE_ERROR;

      info->finished_stream = NULL;
    }
  else if ((s1 = st_str_has_prefix_span(line, "<TD  CLASS=\"title"))
      && (s2 = st_strstr_span(s1, "href='"))
      && (s3 = strstr(s2, "'>"))
      && (s4 = strstr(s3, "</a>")))
    {
      if (info->stream)
	{
	  g_free(info->stream->homepage);
	  info->stream->homepage = st_sgml_ref_expand_len(s2, s3 - s2);
	  g_free(info->stream->title);
	  s3 += 2; info->stream->title = st_sgml_ref_expand_len(s3, s4 - s3);
	}
      else
	PARSE_ERROR;
    }
  else if ((s1 = st_str_has_prefix_span(line, "<TD  CLASS=\"genre\" >"))
	   && (s2 = strstr(s1, "</TD>")))
    {
      if (info->stream)
	{
	  g_free(info->stream->genre);
	  info->stream->genre = st_sgml_ref_expand_len(s1, s2 - s1);
	}
      else
	PARSE_ERROR;
    }
  else if ((s1 = st_strstr_span(line, "&station_id="))
	   && (s2 = strchr(s1, '&')))
    {
      if (info->stream)
	{
	  word1 = g_strndup(s1, s2 - s1);
	  if (st_str_like(word1, ST_NUMERIC))
	    info->stream->station_id = atoi(word1);
	  g_free(word1);
	}
      else
	PARSE_ERROR;
    }
  else if ((s1 = st_strstr_span(line, "alt=\"More broadcaster info"))
	   && (s2 = st_strchr_span(s1, '>'))
	   && (s3 = strstr(s2, "</a>")))
    {
      if (info->stream)
	{
	  g_free(info->stream->broadcaster);
	  info->stream->broadcaster = st_sgml_ref_expand_len(s2, s3 - s2);
	}
      else
	PARSE_ERROR;
    }
  else if ((s1 = st_str_has_prefix_span(line, "<TD  CLASS=\"connection\""))
	   && (s2 = st_strchr_span(s1, '>'))
	   && (s3 = strstr(s2, "<br>"))
	   && ((s4 = strstr(s3, "<img src='/images/mp3pro"))
	       || (s4 = strstr(s3, "</TD>"))))
    {
      if (info->stream)
	{
	  word1 = st_sgml_ref_expand_len(s2, s3 - s2);
	  s3 += 4; word2 = st_sgml_ref_expand_len(s3, s4 - s3);
	  
	  g_free(info->stream->audio);
	  info->stream->audio = g_strdup_printf(g_str_has_prefix(s4, "</TD>") ? "%s %s" : "%s %s, MP3Pro", word2, word1);

	  g_free(word1);
	  g_free(word2);
	}
      else
	PARSE_ERROR;
    }
  else if ((s1 = st_str_has_prefix_span(line, "<TD  CLASS=\"rating\""))
	   && (s2 = st_strstr_span(s1, "DrawListenerStars("))
	   && (s3 = strchr(s2, ','))
	   && (s4 = st_strstr_span(s3, "DrawRatingStars("))
	   && (s5 = strchr(s4, ','))
	   && (s6 = st_strchr_span(s5, ' '))
	   && (s7 = strchr(s6, ',')))
    {
      if (info->stream)
	{
	  word1 = g_strndup(s2, s3 - s2);
	  word2 = g_strndup(s4, s5 - s4);
	  word3 = g_strndup(s6, s7 - s6);

	  if (st_str_like(word1, ST_NUMERIC))
	    info->stream->tlh = atoi(word1);
	  else
	    PARSE_ERROR;

	  if (st_str_like(word2, ST_NUMERIC) && st_str_like(word3, ST_NUMERIC))
	    {
	      int total = atoi(word2);
	      int count = atoi(word3);

	      if (count > 0)
		info->stream->rating = (double) total / count;
	    }
	  else
	    PARSE_ERROR;

	  g_free(word1);
	  g_free(word2);
	  g_free(word3);
	}
      else
	PARSE_ERROR;
    }
  else if (strstr(line, "TD  CLASS=\"tabledesc2\""))
    {
      if (info->stream)
	{
	  if (info->stream->station_id)
	    {
	      ((STStream *) info->stream)->name = g_strdup_printf("%i", info->stream->station_id);
	      *(info->streams) = g_list_append(*(info->streams), info->stream);
	    }
	  else
	    {
	      PARSE_ERROR;
	      stream_free_cb(info->stream, NULL);
	    }

	  info->finished_stream = info->stream;
	  info->stream = NULL;
	}
      else
	PARSE_ERROR;
    }
  else if ((s1 = st_strstr_span(line, "<a class='desc-link' href='"))
	   && (s2 = st_strchr_span(s1, '>'))
	   && (s3 = strstr(s2, "<BR>")))
    {
      if (info->finished_stream)
	{
	  info->finished_stream->description = st_sgml_ref_expand_len(s2, s3 - s2);
	  info->finished_stream = NULL;
	}
      else
	PARSE_ERROR;
    }
  else if (strstr(line, "Next</A>"))
    info->has_next_page = TRUE;
}

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

  str = st_search_dialog();
  if (str)
    {
      char *escaped;

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

      escaped = st_transfer_escape(str);
      g_free(str);

      g_free(category->url_postfix);
      category->url_postfix = g_strconcat(DIRECTORY_PREFIX,
					  "search&searchdesc=",
					  escaped,
					  NULL);
      g_free(escaped);

      return TRUE;
    }
  else
    return FALSE;
}

static GtkWidget *
preferences_widget_new_cb (gpointer data)
{
  GtkWidget *widget;
  GtkWidget *hbox1;
  GtkWidget *hbox2;
  GtkWidget *vbox1;
  GtkWidget *section;
  GtkSizeGroup *size_group;
  char *name;
  char *password;

  widget = gtk_vbox_new(FALSE, 18);

  hbox1 = gtk_hbox_new(FALSE, 12);

  preferences_stream_limit_check = gtk_check_button_new_with_mnemonic(_("_Load at most:"));
  gtk_box_pack_start(GTK_BOX(hbox1), preferences_stream_limit_check, FALSE, FALSE, 0);

  hbox2 = gtk_hbox_new(FALSE, 6);
  preferences_stream_limit_spin = gtk_spin_button_new_with_range(MIN_STREAM_LIMIT, MAX_STREAM_LIMIT, 1);
  preferences_stream_limit_label = gtk_label_new(_("streams per category"));
  gtk_box_pack_start(GTK_BOX(hbox2), preferences_stream_limit_spin, FALSE, FALSE, 0);
  gtk_box_pack_start(GTK_BOX(hbox2), preferences_stream_limit_label, FALSE, FALSE, 0);

  gtk_box_pack_start(GTK_BOX(hbox1), hbox2, FALSE, FALSE, 0);

  section = st_hig_section_new(_("Streams Limit"), hbox1);
  gtk_widget_show_all(section);
  gtk_box_pack_start(GTK_BOX(widget), section, FALSE, FALSE, 0);

  vbox1 = gtk_vbox_new(FALSE, 6);

  preferences_use_membership_check = gtk_check_button_new_with_mnemonic(_("_Use membership"));
  gtk_box_pack_start(GTK_BOX(vbox1), preferences_use_membership_check, FALSE, FALSE, 0);

  hbox1 = gtk_hbox_new(FALSE, 12);
  preferences_name_label = gtk_label_new_with_mnemonic(_("_Name:"));
  preferences_name_entry = gtk_entry_new();
  gtk_box_pack_start(GTK_BOX(hbox1), preferences_name_label, FALSE, FALSE, 0);
  gtk_box_pack_start(GTK_BOX(hbox1), preferences_name_entry, TRUE, TRUE, 0);
  gtk_box_pack_start(GTK_BOX(vbox1), hbox1, FALSE, FALSE, 0);

  hbox1 = gtk_hbox_new(FALSE, 12);
  preferences_password_label = gtk_label_new_with_mnemonic(_("_Password:"));
  preferences_password_entry = gtk_entry_new();
  gtk_box_pack_start(GTK_BOX(hbox1), preferences_password_label, FALSE, FALSE, 0);
  gtk_box_pack_start(GTK_BOX(hbox1), preferences_password_entry, TRUE, TRUE, 0);
  gtk_box_pack_start(GTK_BOX(vbox1), hbox1, FALSE, FALSE, 0);

  gtk_misc_set_alignment(GTK_MISC(preferences_name_label), 1, 0.5);
  gtk_misc_set_alignment(GTK_MISC(preferences_password_label), 1, 0.5);

  gtk_label_set_mnemonic_widget(GTK_LABEL(preferences_name_label), preferences_name_entry);
  gtk_label_set_mnemonic_widget(GTK_LABEL(preferences_password_label), preferences_password_entry);

  gtk_entry_set_visibility(GTK_ENTRY(preferences_password_entry), FALSE);

  section = st_hig_section_new(_("Membership"), vbox1);
  gtk_widget_show_all(section);
  gtk_box_pack_start(GTK_BOX(widget), section, FALSE, FALSE, 0);

  size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
  gtk_size_group_add_widget(size_group, preferences_stream_limit_check);
  gtk_size_group_add_widget(size_group, preferences_name_label);
  gtk_size_group_add_widget(size_group, preferences_password_label);
  g_object_unref(size_group);

  st_set_tooltip(preferences_stream_limit_check, _("If this option is enabled, the number of streams to download will be limited."));
  st_set_tooltip(preferences_stream_limit_spin, _("The maximum number of streams to download per category"));
  st_set_tooltip(preferences_use_membership_check, _("If this option is enabled, streamtuner will log into Live365 before tuning into streams or recording them."));
  st_set_tooltip(preferences_name_entry,
		 _("Your Live365 member name.\n"
		   "\n"
		   "If left blank, you will be prompted for your member name when needed."));
  st_set_tooltip(preferences_password_entry,
		 _("Your Live365 password.\n"
		   "\n"
		   "If left blank, you will be prompted for your password when needed."));

  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(preferences_stream_limit_check), st_handler_config_get_boolean(live365_handler, CONFIG_STREAM_LIMIT_ENABLED));
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(preferences_stream_limit_spin), st_handler_config_get_int(live365_handler, CONFIG_STREAM_LIMIT));

  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(preferences_use_membership_check), st_handler_config_get_boolean(live365_handler, CONFIG_USE_MEMBERSHIP));

  name = st_handler_config_get_string(live365_handler, CONFIG_NAME);
  password = st_handler_config_get_string(live365_handler, CONFIG_PASSWORD);

  gtk_entry_set_text(GTK_ENTRY(preferences_name_entry), name ? name : "");
  gtk_entry_set_text(GTK_ENTRY(preferences_password_entry), password ? password : "");

  g_free(name);
  g_free(password);

  preferences_update_sensitivity();

  g_signal_connect(preferences_stream_limit_check, "toggled", G_CALLBACK(preferences_stream_limit_toggled_h), NULL);
  g_signal_connect(preferences_stream_limit_spin, "value-changed", G_CALLBACK(preferences_stream_limit_changed_h), NULL);
  g_signal_connect(preferences_use_membership_check, "toggled", G_CALLBACK(preferences_use_membership_toggled_h), NULL);

  g_object_connect(preferences_name_entry,
		   "signal::activate", preferences_credentials_activate_h, preferences_password_entry,
		   "signal::focus-out-event", preferences_credentials_focus_out_event_h, CONFIG_NAME,
		   NULL);
  g_object_connect(preferences_password_entry,
		   "signal::activate", preferences_credentials_activate_h, preferences_name_entry,
		   "signal::focus-out-event", preferences_credentials_focus_out_event_h, CONFIG_PASSWORD,
		   NULL);

  return widget;
}

static void
preferences_update_sensitivity (void)
{
  gboolean stream_limit_enabled;
  gboolean use_membership;

  stream_limit_enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(preferences_stream_limit_check));
  gtk_widget_set_sensitive(preferences_stream_limit_spin, stream_limit_enabled);
  gtk_widget_set_sensitive(preferences_stream_limit_label, stream_limit_enabled);

  use_membership = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(preferences_use_membership_check));
  gtk_widget_set_sensitive(preferences_name_label, use_membership);
  gtk_widget_set_sensitive(preferences_name_entry, use_membership);
  gtk_widget_set_sensitive(preferences_password_label, use_membership);
  gtk_widget_set_sensitive(preferences_password_entry, use_membership);
}

static void
preferences_stream_limit_toggled_h (GtkToggleButton *button, gpointer user_data)
{
  st_handler_config_set_boolean(live365_handler, CONFIG_STREAM_LIMIT_ENABLED, gtk_toggle_button_get_active(button));
  preferences_update_sensitivity();
}

static void
preferences_stream_limit_changed_h (GtkSpinButton *spin, gpointer user_data)
{
  st_handler_config_set_int(live365_handler, CONFIG_STREAM_LIMIT, gtk_spin_button_get_value_as_int(spin));
}

static void
preferences_use_membership_toggled_h (GtkToggleButton *button, gpointer user_data)
{
  st_handler_config_set_boolean(live365_handler, CONFIG_USE_MEMBERSHIP, gtk_toggle_button_get_active(button));
  preferences_update_sensitivity();
}

static void
preferences_credentials_activate_h (GtkEntry *entry, gpointer user_data)
{
  GtkWidget *next = user_data;

  gtk_widget_grab_focus(next);
}

static gboolean
preferences_credentials_focus_out_event_h (GtkWidget *widget,
					   GdkEventFocus *event,
					   gpointer user_data)
{
  const char *key = user_data;
  const char *str;

  str = gtk_entry_get_text(GTK_ENTRY(widget));
  st_handler_config_set_string(live365_handler, key, *str ? str : NULL);

  return FALSE;			/* propagate event */
}

static void
init_handler (void)
{
  GNode *stock_categories;
  STCategory *category;
  STHandlerField *field;
  const char *default_user = NULL;

  live365_handler = st_handler_new_from_plugin(live365_plugin);

  st_handler_set_description(live365_handler, "Live365 Internet Radio");
  st_handler_set_home(live365_handler, LIVE365_ROOT);

  stock_categories = g_node_new(NULL);

  category = st_category_new();
  category->name = "__main";
  category->label = _("Editor's Picks");
  category->url_postfix = DIRECTORY_PREFIX "ESP";
  
  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);
  
  st_handler_set_stock_categories(live365_handler, stock_categories);

  st_handler_bind(live365_handler, ST_HANDLER_EVENT_RELOAD, reload_cb, NULL);

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

  st_handler_bind(live365_handler, ST_HANDLER_EVENT_STREAM_TUNE_IN, stream_tune_in_cb, NULL);
  st_handler_bind(live365_handler, ST_HANDLER_EVENT_STREAM_RECORD, stream_record_cb, NULL);
  st_handler_bind(live365_handler, ST_HANDLER_EVENT_STREAM_BROWSE, stream_browse_cb, NULL);

  st_handler_bind(live365_handler, ST_HANDLER_EVENT_PREFERENCES_WIDGET_NEW, preferences_widget_new_cb, NULL);

  field = st_handler_field_new(FIELD_TITLE,
			       _("Title"),
			       G_TYPE_STRING,
			       ST_HANDLER_FIELD_VISIBLE);
  st_handler_field_set_description(field, _("The stream title"));
  st_handler_add_field(live365_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(live365_handler, field);

  field = st_handler_field_new(FIELD_DESCRIPTION,
			       _("Description"),
			       G_TYPE_STRING,
			       ST_HANDLER_FIELD_VISIBLE);
  st_handler_field_set_description(field, _("The stream description"));
  st_handler_add_field(live365_handler, field);

  field = st_handler_field_new(FIELD_BROADCASTER,
			       _("Broadcaster"),
			       G_TYPE_STRING,
			       ST_HANDLER_FIELD_VISIBLE);
  st_handler_field_set_description(field, _("The stream broadcaster"));
  st_handler_add_field(live365_handler, field);

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

  field = st_handler_field_new(FIELD_HOMEPAGE,
			       _("Homepage"),
			       G_TYPE_STRING,
			       ST_HANDLER_FIELD_VISIBLE
			       | ST_HANDLER_FIELD_START_HIDDEN);
  st_handler_field_set_description(field, _("The stream homepage URL"));
  st_handler_add_field(live365_handler, field);

  st_handler_add_field(live365_handler, st_handler_field_new(FIELD_STATION_ID,
							     _("Station ID"),
							     G_TYPE_INT,
							     0));

  field = st_handler_field_new(FIELD_URL,
			       _("URL"),
			       G_TYPE_STRING,
			       ST_HANDLER_FIELD_VISIBLE
			       | ST_HANDLER_FIELD_START_HIDDEN
			       | ST_HANDLER_FIELD_VOLATILE);
  st_handler_field_set_description(field, _("The stream listen URL"));
  st_handler_add_field(live365_handler, field);
  
  st_handler_add_field(live365_handler, st_handler_field_new(FIELD_ACCESS,
							     _("Access"),
							     G_TYPE_INT,
							     0));

  field = st_handler_field_new(FIELD_ACCESS_STRING,
			       _("Access"),
			       G_TYPE_STRING,
			       ST_HANDLER_FIELD_VISIBLE
			       | ST_HANDLER_FIELD_START_HIDDEN
			       | ST_HANDLER_FIELD_VOLATILE);
  st_handler_field_set_description(field, _("The type of members allowed to tune into the stream"));
  st_handler_add_field(live365_handler, field);

  field = st_handler_field_new(FIELD_TLH,
			       /* translator: "TLH" means "Total Listening Hours", translate the acronym properly */
			       _("TLH"),
			       G_TYPE_INT,
			       ST_HANDLER_FIELD_VISIBLE
			       | ST_HANDLER_FIELD_START_HIDDEN);
  st_handler_field_set_description(field, _("The stream total listening hours"));
  st_handler_add_field(live365_handler, field);

  st_handler_add_field(live365_handler, st_handler_field_new(FIELD_RATING,
							     _("Rating"),
							     G_TYPE_DOUBLE,
							     0));

  field = st_handler_field_new(FIELD_RATING_STRING,
			       _("Rating"),
			       G_TYPE_STRING,
			       ST_HANDLER_FIELD_VISIBLE
			       | ST_HANDLER_FIELD_START_HIDDEN
			       | ST_HANDLER_FIELD_VOLATILE);
  st_handler_field_set_description(field, _("The stream rating"));
  st_handler_add_field(live365_handler, field);

  default_user = g_getenv("STREAMTUNER_LIVE365_USER");
  if (default_user)
    st_handler_notice(live365_handler, _("the STREAMTUNER_LIVE365_USER environment variable is deprecated"));

  st_handler_config_register(live365_handler,
			     g_param_spec_boolean(CONFIG_USE_MEMBERSHIP,
						  NULL,
						  NULL,
						  FALSE,
						  G_PARAM_READWRITE));
  st_handler_config_register(live365_handler,
			     g_param_spec_string(CONFIG_NAME,
						 NULL,
						 NULL,
						 default_user,
						 G_PARAM_READWRITE));
  st_handler_config_register(live365_handler,
			     g_param_spec_string(CONFIG_PASSWORD,
						 NULL,
						 NULL,
						 NULL,
						 G_PARAM_READWRITE));
  st_handler_config_register(live365_handler,
			     g_param_spec_boolean(CONFIG_STREAM_LIMIT_ENABLED,
						  NULL,
						  NULL,
						  FALSE,
						  G_PARAM_READWRITE));
  st_handler_config_register(live365_handler,
			     g_param_spec_int(CONFIG_STREAM_LIMIT,
					      NULL,
					      NULL,
					      MIN_STREAM_LIMIT,
					      MAX_STREAM_LIMIT,
					      100,
					      G_PARAM_READWRITE));

  if (g_getenv("STREAMTUNER_LIVE365_SESSION"))
    st_handler_notice(live365_handler, _("the STREAMTUNER_LIVE365_SESSION environment variable is obsolete, ignored"));

  st_handlers_add(live365_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;

  live365_plugin = plugin;

  st_plugin_set_name(plugin, "live365");
  st_plugin_set_label(plugin, "Live365");

  pixbuf = st_pixbuf_new_from_file(UIDIR "/live365.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;

  init_handler();

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

  return TRUE;
}
