/* * 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 #include #include #include "streamtuner.h" /*** cpp *********************************************************************/ #define SHOUTCAST_ROOT "http://www.shoutcast.com/" #define MAX_STREAMS_PER_PAGE 100 /* enforced by SHOUTcast */ #define PARSE_ERROR st_handler_notice(shoutcast_handler, _("parse error at %s"), G_STRLOC) #define CONFIG_STREAM_LIMIT_ENABLED "stream-limit-enabled" #define CONFIG_STREAM_LIMIT "stream-limit" #define MIN_STREAM_LIMIT 0 #define MAX_STREAM_LIMIT 9999 /*** type definitions ********************************************************/ typedef struct { STStream stream; char *genre; char *description; char *now_playing; int listeners; int max; int bitrate; char *url_postfix; char *homepage; /* * url_list may be set after the stream has entered streamtuner, so * we need to protect it. */ GSList *url_list; GMutex *url_list_mutex; } SHOUTcastStream; enum { FIELD_GENRE, FIELD_DESCRIPTION, FIELD_NOW_PLAYING, FIELD_LISTENERS, FIELD_MAX, FIELD_BITRATE, FIELD_URL_POSTFIX, FIELD_HOMEPAGE, FIELD_URL_LIST }; typedef struct { GNode **categories; GList **streams; int page; int npages; GNode *parent_node; SHOUTcastStream *stream; } ReloadInfo; /*** variable declarations ***************************************************/ static STPlugin *shoutcast_plugin = NULL; static STHandler *shoutcast_handler = NULL; static GtkWidget *preferences_stream_limit_check; static GtkWidget *preferences_stream_limit_spin; static GtkWidget *preferences_stream_limit_label; /*** function declarations ***************************************************/ static gboolean reload_cb (STCategory *category, GNode **categories, GList **streams, gpointer data, GError **err); static void reload_body_cb (const char *line, gpointer data); static SHOUTcastStream *stream_new_cb (gpointer data); static void stream_field_get_cb (SHOUTcastStream *stream, STHandlerField *field, GValue *value, gpointer data); static void stream_field_set_cb (SHOUTcastStream *stream, STHandlerField *field, const GValue *value, gpointer data); static void stream_stock_field_get_cb (SHOUTcastStream *stream, STHandlerStockField stock_field, GValue *value, gpointer data); static void stream_free_cb (SHOUTcastStream *stream, gpointer data); static void stream_get_url_list (SHOUTcastStream *stream, GValue *value); static gboolean stream_resolve (SHOUTcastStream *stream, GError **err); static gboolean stream_resolve_cb (SHOUTcastStream *stream, gpointer data, GError **err); static gboolean stream_tune_in_cb (SHOUTcastStream *stream, gpointer data, GError **err); static gboolean stream_record_cb (SHOUTcastStream *stream, gpointer data, GError **err); static gboolean stream_browse_cb (SHOUTcastStream *stream, gpointer data, GError **err); static int 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 init_handler (void); static gboolean check_api_version (GError **err); /*** implementation **********************************************************/ static SHOUTcastStream * stream_new_cb (gpointer data) { SHOUTcastStream *stream; stream = g_new0(SHOUTcastStream, 1); stream->url_list_mutex = g_mutex_new(); return stream; } static void stream_field_get_cb (SHOUTcastStream *stream, STHandlerField *field, GValue *value, gpointer data) { switch (field->id) { case FIELD_GENRE: g_value_set_string(value, stream->genre); break; case FIELD_DESCRIPTION: g_value_set_string(value, stream->description); break; case FIELD_NOW_PLAYING: g_value_set_string(value, stream->now_playing); break; case FIELD_LISTENERS: g_value_set_int(value, stream->listeners); break; case FIELD_MAX: g_value_set_int(value, stream->max); break; case FIELD_BITRATE: g_value_set_int(value, stream->bitrate); break; case FIELD_URL_POSTFIX: g_value_set_string(value, stream->url_postfix); break; case FIELD_HOMEPAGE: g_value_set_string(value, stream->homepage); break; case FIELD_URL_LIST: stream_get_url_list(stream, value); break; default: g_return_if_reached(); } } static void stream_field_set_cb (SHOUTcastStream *stream, STHandlerField *field, const GValue *value, gpointer data) { switch (field->id) { case FIELD_GENRE: stream->genre = g_value_dup_string(value); break; case FIELD_DESCRIPTION: stream->description = g_value_dup_string(value); break; case FIELD_NOW_PLAYING: stream->now_playing = g_value_dup_string(value); break; case FIELD_LISTENERS: stream->listeners = g_value_get_int(value); break; case FIELD_MAX: stream->max = g_value_get_int(value); break; case FIELD_BITRATE: stream->bitrate = g_value_get_int(value); break; case FIELD_URL_POSTFIX: stream->url_postfix = g_value_dup_string(value); break; case FIELD_HOMEPAGE: stream->homepage = g_value_dup_string(value); break; case FIELD_URL_LIST: { GValueArray *value_array; int i; value_array = g_value_get_boxed(value); for (i = 0; i < value_array->n_values; i++) { GValue *url_value = g_value_array_get_nth(value_array, i); stream->url_list = g_slist_append(stream->url_list, g_value_dup_string(url_value)); } break; } default: g_return_if_reached(); } } static void stream_stock_field_get_cb (SHOUTcastStream *stream, STHandlerStockField stock_field, GValue *value, gpointer data) { switch (stock_field) { case ST_HANDLER_STOCK_FIELD_NAME: g_value_set_string(value, stream->description); break; case ST_HANDLER_STOCK_FIELD_GENRE: g_value_set_string(value, stream->genre); break; case ST_HANDLER_STOCK_FIELD_DESCRIPTION: /* nop */ break; case ST_HANDLER_STOCK_FIELD_HOMEPAGE: g_value_set_string(value, stream->homepage); break; case ST_HANDLER_STOCK_FIELD_URI_LIST: stream_get_url_list(stream, value); break; } } static void stream_free_cb (SHOUTcastStream *stream, gpointer data) { GSList *l; g_free(stream->genre); g_free(stream->description); g_free(stream->now_playing); g_free(stream->url_postfix); g_free(stream->homepage); for (l = stream->url_list; l; l = l->next) g_free(l->data); g_slist_free(stream->url_list); g_mutex_free(stream->url_list_mutex); st_stream_free((STStream *) stream); } static void stream_get_url_list (SHOUTcastStream *stream, GValue *value) { GValueArray *value_array; GSList *l; g_return_if_fail(stream != NULL); g_return_if_fail(value != NULL); value_array = g_value_array_new(0); g_mutex_lock(stream->url_list_mutex); for (l = stream->url_list; l; l = l->next) { GValue url_value = { 0, }; g_value_init(&url_value, G_TYPE_STRING); g_value_set_string(&url_value, l->data); g_value_array_append(value_array, &url_value); g_value_unset(&url_value); } g_mutex_unlock(stream->url_list_mutex); g_value_take_boxed(value, value_array); } static gboolean stream_resolve (SHOUTcastStream *stream, GError **err) { gboolean already_resolved; STTransferSession *session; char *url; char *playlist; gboolean status; g_return_val_if_fail(stream != NULL, FALSE); g_mutex_lock(stream->url_list_mutex); already_resolved = stream->url_list != NULL; g_mutex_unlock(stream->url_list_mutex); if (already_resolved) return TRUE; /* already resolved */ url = g_strconcat(SHOUTCAST_ROOT, stream->url_postfix, NULL); session = st_transfer_session_new(); status = st_transfer_session_get(session, url, ST_TRANSFER_UTF8, NULL, &playlist, err); st_transfer_session_free(session); g_free(url); if (status) { gboolean empty; g_mutex_lock(stream->url_list_mutex); stream->url_list = st_pls_parse(playlist); empty = stream->url_list == NULL; g_mutex_unlock(stream->url_list_mutex); g_free(playlist); if (empty) { g_set_error(err, 0, 0, _("stream is empty")); return FALSE; } } return status; } static gboolean stream_resolve_cb (SHOUTcastStream *stream, gpointer data, GError **err) { return stream_resolve(stream, err); } static gboolean stream_tune_in_cb (SHOUTcastStream *stream, gpointer data, GError **err) { char *m3uname; gboolean status; if (! stream_resolve(stream, err)) return FALSE; g_mutex_lock(stream->url_list_mutex); m3uname = st_m3u_mktemp("streamtuner.shoutcast.XXXXXX", stream->url_list, err); g_mutex_unlock(stream->url_list_mutex); if (! m3uname) return FALSE; status = st_action_run("play-m3u", m3uname, err); g_free(m3uname); return status; } static gboolean stream_record_cb (SHOUTcastStream *stream, gpointer data, GError **err) { gboolean status; if (! stream_resolve(stream, err)) return FALSE; g_mutex_lock(stream->url_list_mutex); status = st_action_run("record-stream", stream->url_list->data, err); g_mutex_unlock(stream->url_list_mutex); return status; } static gboolean stream_browse_cb (SHOUTcastStream *stream, gpointer data, GError **err) { if (! stream->homepage) /* older versions of the plugin didn't have this field */ { g_set_error(err, 0, 0, _("the stream is too old, please reload")); return FALSE; } return st_action_run("view-web", stream->homepage, err); } static gboolean reload_cb (STCategory *category, GNode **categories, GList **streams, gpointer data, GError **err) { ReloadInfo info; STTransferSession *session; gboolean status; int stream_limit; int requested_streams = 0; int received_streams = 0; g_return_val_if_fail(category != NULL, FALSE); g_return_val_if_fail(category->url_postfix != NULL, FALSE); *categories = g_node_new(NULL); *streams = NULL; info.categories = categories; info.streams = streams; session = st_transfer_session_new(); stream_limit = st_handler_config_get_boolean(shoutcast_handler, CONFIG_STREAM_LIMIT_ENABLED) ? st_handler_config_get_int(shoutcast_handler, CONFIG_STREAM_LIMIT) : (! strcmp(category->name, "__main") ? 500 : -1); /* [1] */ /* * [1] The main category contains all the SHOUTcast streams (a lot), * so we just load the first 500. */ 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(SHOUTCAST_ROOT "directory/?numresult=%i&startat=%i%s", rows, requested_streams, category->url_postfix); requested_streams += rows; info.page = 0; info.npages = 0; info.parent_node = NULL; info.stream = NULL; 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_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.page > 0 && info.page < info.npages && (stream_limit == -1 || received_streams < stream_limit)); st_transfer_session_free(session); return status; } static void reload_body_cb (const char *line, gpointer data) { ReloadInfo *info = data; char *s1, *s2, *s3, *s4, *s5; char *word1, *word2; if ((s1 = strstr(line, "sbin/shoutcast-playlist.pls")) && (s2 = st_strstr_span(s1, "filename.pls"))) { if (info->stream) /* a malformed stream remains, free it */ { PARSE_ERROR; stream_free_cb(info->stream, NULL); } info->stream = stream_new_cb(NULL); info->stream->url_postfix = st_sgml_ref_expand_len(s1, s2 - s1); } else if (info->page < 2 && (((s1 = st_str_has_prefix_span(line, "\t