/* * 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 #include #include #include #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; }