diff --git a/src/plugin_xmms/Makefile.am b/src/plugin_xmms/Makefile.am index d639926d..4421e90e 100644 --- a/src/plugin_xmms/Makefile.am +++ b/src/plugin_xmms/Makefile.am @@ -25,6 +25,8 @@ EXTRA_DIST = \ noinst_HEADERS = \ charset.h \ configure.h \ + http.h \ + plugin.h \ tag.h CFLAGS = @CFLAGS@ @XMMS_CFLAGS@ @@ -40,7 +42,7 @@ LIBTOOL = $(top_builddir)/libtool-disable-static xmmsinputplugin_LTLIBRARIES = libxmms-flac.la -plugin_sources = charset.c configure.c plugin.c tag.c fileinfo.c +plugin_sources = charset.c configure.c fileinfo.c http.c plugin.c tag.c libxmms_flac_la_SOURCES = $(plugin_sources) diff --git a/src/plugin_xmms/Makefile.lite b/src/plugin_xmms/Makefile.lite index e5763db5..05aa9b64 100644 --- a/src/plugin_xmms/Makefile.lite +++ b/src/plugin_xmms/Makefile.lite @@ -31,6 +31,7 @@ SRCS_C = \ configure.c \ plugin.c \ fileinfo.c \ + http.c \ tag.c include $(topdir)/build/lib.mk diff --git a/src/plugin_xmms/configure.c b/src/plugin_xmms/configure.c index c04bb010..82ba8626 100644 --- a/src/plugin_xmms/configure.c +++ b/src/plugin_xmms/configure.c @@ -50,6 +50,21 @@ flac_config_t flac_cfg = { FALSE, /* convert_char_set */ NULL /* user_char_set */ }, + /* stream */ + { + 100 /* KB */, /* http_buffer_size */ + 50, /* http_prebuffer */ + FALSE, /* use_proxy */ + "", /* proxy_host */ + 0, /* proxy_port */ + FALSE, /* proxy_use_auth */ + "", /* proxy_user */ + "", /* proxy_pass */ + FALSE, /* save_http_stream */ + "", /* save_http_path */ + FALSE, /* cast_title_streaming */ + FALSE /* use_udp_channel */ + }, /* output */ { /* replaygain */ @@ -95,6 +110,18 @@ static GtkWidget *resolution_replaygain_bps_out_frame; static GtkWidget *resolution_replaygain_bps_out_radio_16bps; static GtkWidget *resolution_replaygain_bps_out_radio_24bps; +static GtkObject *streaming_size_adj, *streaming_pre_adj; +static GtkWidget *streaming_proxy_use, *streaming_proxy_host_entry; +static GtkWidget *streaming_proxy_port_entry, *streaming_save_use, *streaming_save_entry; +static GtkWidget *streaming_proxy_auth_use; +static GtkWidget *streaming_proxy_auth_pass_entry, *streaming_proxy_auth_user_entry; +static GtkWidget *streaming_proxy_auth_user_label, *streaming_proxy_auth_pass_label; +#ifdef FLAC_ICECAST +static GtkWidget *streaming_cast_title, *streaming_udp_title; +#endif +static GtkWidget *streaming_proxy_hbox, *streaming_proxy_auth_hbox, *streaming_save_dirbrowser; +static GtkWidget *streaming_save_hbox; + static gchar *gtk_entry_get_text_1 (GtkWidget *widget); static void flac_configurewin_ok(GtkWidget * widget, gpointer data); static void configure_destroy(GtkWidget * w, gpointer data); @@ -127,6 +154,61 @@ static void flac_configurewin_ok(GtkWidget * widget, gpointer data) xmms_cfg_write_boolean(cfg, "flac", "output.resolution.replaygain.dither", flac_cfg.output.resolution.replaygain.dither); xmms_cfg_write_int(cfg, "flac", "output.resolution.replaygain.noise_shaping", flac_cfg.output.resolution.replaygain.noise_shaping); xmms_cfg_write_int(cfg, "flac", "output.resolution.replaygain.bps_out", flac_cfg.output.resolution.replaygain.bps_out); + /* streaming */ + flac_cfg.stream.http_buffer_size = (gint) GTK_ADJUSTMENT(streaming_size_adj)->value; + flac_cfg.stream.http_prebuffer = (gint) GTK_ADJUSTMENT(streaming_pre_adj)->value; + + flac_cfg.stream.use_proxy = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(streaming_proxy_use)); + g_free(flac_cfg.stream.proxy_host); + flac_cfg.stream.proxy_host = g_strdup(gtk_entry_get_text(GTK_ENTRY(streaming_proxy_host_entry))); + flac_cfg.stream.proxy_port = atoi(gtk_entry_get_text(GTK_ENTRY(streaming_proxy_port_entry))); + + flac_cfg.stream.proxy_use_auth = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(streaming_proxy_auth_use)); + + if(flac_cfg.stream.proxy_user) + g_free(flac_cfg.stream.proxy_user); + flac_cfg.stream.proxy_user = NULL; + if(strlen(gtk_entry_get_text(GTK_ENTRY(streaming_proxy_auth_user_entry))) > 0) + flac_cfg.stream.proxy_user = g_strdup(gtk_entry_get_text(GTK_ENTRY(streaming_proxy_auth_user_entry))); + + if(flac_cfg.stream.proxy_pass) + g_free(flac_cfg.stream.proxy_pass); + flac_cfg.stream.proxy_pass = NULL; + if(strlen(gtk_entry_get_text(GTK_ENTRY(streaming_proxy_auth_pass_entry))) > 0) + flac_cfg.stream.proxy_pass = g_strdup(gtk_entry_get_text(GTK_ENTRY(streaming_proxy_auth_pass_entry))); + + + flac_cfg.stream.save_http_stream = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(streaming_save_use)); + if (flac_cfg.stream.save_http_path) + g_free(flac_cfg.stream.save_http_path); + flac_cfg.stream.save_http_path = g_strdup(gtk_entry_get_text(GTK_ENTRY(streaming_save_entry))); + +#ifdef FLAC_ICECAST + flac_cfg.stream.cast_title_streaming = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(streaming_cast_title)); + flac_cfg.stream.use_udp_channel = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(streaming_udp_title)); +#endif + + xmms_cfg_write_int(cfg, "flac", "stream.http_buffer_size", flac_cfg.stream.http_buffer_size); + xmms_cfg_write_int(cfg, "flac", "stream.http_prebuffer", flac_cfg.stream.http_prebuffer); + xmms_cfg_write_boolean(cfg, "flac", "stream.use_proxy", flac_cfg.stream.use_proxy); + xmms_cfg_write_string(cfg, "flac", "stream.proxy_host", flac_cfg.stream.proxy_host); + xmms_cfg_write_int(cfg, "flac", "stream.proxy_port", flac_cfg.stream.proxy_port); + xmms_cfg_write_boolean(cfg, "flac", "stream.proxy_use_auth", flac_cfg.stream.proxy_use_auth); + if(flac_cfg.stream.proxy_user) + xmms_cfg_write_string(cfg, "flac", "stream.proxy_user", flac_cfg.stream.proxy_user); + else + xmms_cfg_remove_key(cfg, "flac", "stream.proxy_user"); + if(flac_cfg.stream.proxy_pass) + xmms_cfg_write_string(cfg, "flac", "stream.proxy_pass", flac_cfg.stream.proxy_pass); + else + xmms_cfg_remove_key(cfg, "flac", "stream.proxy_pass"); + xmms_cfg_write_boolean(cfg, "flac", "stream.save_http_stream", flac_cfg.stream.save_http_stream); + xmms_cfg_write_string(cfg, "flac", "stream.save_http_path", flac_cfg.stream.save_http_path); +#ifdef FLAC_ICECAST + xmms_cfg_write_boolean(cfg, "flac", "stream.cast_title_streaming", flac_cfg.stream.cast_title_streaming); + xmms_cfg_write_boolean(cfg, "flac", "stream.use_udp_channel", flac_cfg.stream.use_udp_channel); +#endif + xmms_cfg_write_file(cfg, filename); xmms_cfg_free(cfg); g_free(filename); @@ -225,6 +307,62 @@ static void resolution_replaygain_bps_out_cb(GtkWidget *widget, gpointer data) ; } +static void proxy_use_cb(GtkWidget * w, gpointer data) +{ + gboolean use_proxy, use_proxy_auth; + (void) w; + (void) data; + + use_proxy = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(streaming_proxy_use)); + use_proxy_auth = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(streaming_proxy_auth_use)); + + gtk_widget_set_sensitive(streaming_proxy_hbox, use_proxy); + gtk_widget_set_sensitive(streaming_proxy_auth_use, use_proxy); + gtk_widget_set_sensitive(streaming_proxy_auth_hbox, use_proxy && use_proxy_auth); +} + +static void proxy_auth_use_cb(GtkWidget *w, gpointer data) +{ + gboolean use_proxy, use_proxy_auth; + (void) w; + (void) data; + + use_proxy = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(streaming_proxy_use)); + use_proxy_auth = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(streaming_proxy_auth_use)); + + gtk_widget_set_sensitive(streaming_proxy_auth_hbox, use_proxy && use_proxy_auth); +} + +static void streaming_save_dirbrowser_cb(gchar * dir) +{ + gtk_entry_set_text(GTK_ENTRY(streaming_save_entry), dir); +} + +static void streaming_save_browse_cb(GtkWidget * w, gpointer data) +{ + (void) w; + (void) data; + if (!streaming_save_dirbrowser) + { + streaming_save_dirbrowser = xmms_create_dir_browser(_("Select the directory where you want to store the MPEG streams:"), + flac_cfg.stream.save_http_path, GTK_SELECTION_SINGLE, streaming_save_dirbrowser_cb); + gtk_signal_connect(GTK_OBJECT(streaming_save_dirbrowser), "destroy", GTK_SIGNAL_FUNC(gtk_widget_destroyed), &streaming_save_dirbrowser); + gtk_window_set_transient_for(GTK_WINDOW(streaming_save_dirbrowser), GTK_WINDOW(flac_configurewin)); + gtk_widget_show(streaming_save_dirbrowser); + } +} + +static void streaming_save_use_cb(GtkWidget * w, gpointer data) +{ + gboolean save_stream; + (void) w; + (void) data; + + save_stream = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(streaming_save_use)); + + gtk_widget_set_sensitive(streaming_save_hbox, save_stream); +} + void FLAC_XMMS__configure(void) { @@ -237,6 +375,19 @@ void FLAC_XMMS__configure(void) GtkWidget *bbox, *ok, *cancel; GList *list; + GtkWidget *streaming_vbox; + GtkWidget *streaming_buf_frame, *streaming_buf_hbox; + GtkWidget *streaming_size_box, *streaming_size_label, *streaming_size_spin; + GtkWidget *streaming_pre_box, *streaming_pre_label, *streaming_pre_spin; + GtkWidget *streaming_proxy_frame, *streaming_proxy_vbox; + GtkWidget *streaming_proxy_port_label, *streaming_proxy_host_label; + GtkWidget *streaming_save_frame, *streaming_save_vbox; + GtkWidget *streaming_save_label, *streaming_save_browse; +#ifdef FLAC_ICECAST + GtkWidget *streaming_cast_frame, *streaming_cast_vbox; +#endif + char *temp; + if (flac_configurewin != NULL) { gdk_window_raise(flac_configurewin->window); return; @@ -459,6 +610,153 @@ void FLAC_XMMS__configure(void) gtk_notebook_append_page(GTK_NOTEBOOK(notebook), output_vbox, gtk_label_new(_("Output"))); + /* Streaming */ + + streaming_vbox = gtk_vbox_new(FALSE, 0); + + streaming_buf_frame = gtk_frame_new(_("Buffering:")); + gtk_container_set_border_width(GTK_CONTAINER(streaming_buf_frame), 5); + gtk_box_pack_start(GTK_BOX(streaming_vbox), streaming_buf_frame, FALSE, FALSE, 0); + + streaming_buf_hbox = gtk_hbox_new(TRUE, 5); + gtk_container_set_border_width(GTK_CONTAINER(streaming_buf_hbox), 5); + gtk_container_add(GTK_CONTAINER(streaming_buf_frame), streaming_buf_hbox); + + streaming_size_box = gtk_hbox_new(FALSE, 5); + /*gtk_table_attach_defaults(GTK_TABLE(streaming_buf_table),streaming_size_box,0,1,0,1); */ + gtk_box_pack_start(GTK_BOX(streaming_buf_hbox), streaming_size_box, TRUE, TRUE, 0); + streaming_size_label = gtk_label_new(_("Buffer size (kb):")); + gtk_box_pack_start(GTK_BOX(streaming_size_box), streaming_size_label, FALSE, FALSE, 0); + streaming_size_adj = gtk_adjustment_new(flac_cfg.stream.http_buffer_size, 4, 4096, 4, 4, 4); + streaming_size_spin = gtk_spin_button_new(GTK_ADJUSTMENT(streaming_size_adj), 8, 0); + gtk_widget_set_usize(streaming_size_spin, 60, -1); + gtk_box_pack_start(GTK_BOX(streaming_size_box), streaming_size_spin, FALSE, FALSE, 0); + + streaming_pre_box = gtk_hbox_new(FALSE, 5); + /*gtk_table_attach_defaults(GTK_TABLE(streaming_buf_table),streaming_pre_box,1,2,0,1); */ + gtk_box_pack_start(GTK_BOX(streaming_buf_hbox), streaming_pre_box, TRUE, TRUE, 0); + streaming_pre_label = gtk_label_new(_("Pre-buffer (percent):")); + gtk_box_pack_start(GTK_BOX(streaming_pre_box), streaming_pre_label, FALSE, FALSE, 0); + streaming_pre_adj = gtk_adjustment_new(flac_cfg.stream.http_prebuffer, 0, 90, 1, 1, 1); + streaming_pre_spin = gtk_spin_button_new(GTK_ADJUSTMENT(streaming_pre_adj), 1, 0); + gtk_widget_set_usize(streaming_pre_spin, 60, -1); + gtk_box_pack_start(GTK_BOX(streaming_pre_box), streaming_pre_spin, FALSE, FALSE, 0); + + /* + * Proxy config. + */ + streaming_proxy_frame = gtk_frame_new(_("Proxy:")); + gtk_container_set_border_width(GTK_CONTAINER(streaming_proxy_frame), 5); + gtk_box_pack_start(GTK_BOX(streaming_vbox), streaming_proxy_frame, FALSE, FALSE, 0); + + streaming_proxy_vbox = gtk_vbox_new(FALSE, 5); + gtk_container_set_border_width(GTK_CONTAINER(streaming_proxy_vbox), 5); + gtk_container_add(GTK_CONTAINER(streaming_proxy_frame), streaming_proxy_vbox); + + streaming_proxy_use = gtk_check_button_new_with_label(_("Use proxy")); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(streaming_proxy_use), flac_cfg.stream.use_proxy); + gtk_signal_connect(GTK_OBJECT(streaming_proxy_use), "clicked", GTK_SIGNAL_FUNC(proxy_use_cb), NULL); + gtk_box_pack_start(GTK_BOX(streaming_proxy_vbox), streaming_proxy_use, FALSE, FALSE, 0); + + streaming_proxy_hbox = gtk_hbox_new(FALSE, 5); + gtk_widget_set_sensitive(streaming_proxy_hbox, flac_cfg.stream.use_proxy); + gtk_box_pack_start(GTK_BOX(streaming_proxy_vbox), streaming_proxy_hbox, FALSE, FALSE, 0); + + streaming_proxy_host_label = gtk_label_new(_("Host:")); + gtk_box_pack_start(GTK_BOX(streaming_proxy_hbox), streaming_proxy_host_label, FALSE, FALSE, 0); + + streaming_proxy_host_entry = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(streaming_proxy_host_entry), flac_cfg.stream.proxy_host); + gtk_box_pack_start(GTK_BOX(streaming_proxy_hbox), streaming_proxy_host_entry, TRUE, TRUE, 0); + + streaming_proxy_port_label = gtk_label_new(_("Port:")); + gtk_box_pack_start(GTK_BOX(streaming_proxy_hbox), streaming_proxy_port_label, FALSE, FALSE, 0); + + streaming_proxy_port_entry = gtk_entry_new(); + gtk_widget_set_usize(streaming_proxy_port_entry, 50, -1); + temp = g_strdup_printf("%d", flac_cfg.stream.proxy_port); + gtk_entry_set_text(GTK_ENTRY(streaming_proxy_port_entry), temp); + g_free(temp); + gtk_box_pack_start(GTK_BOX(streaming_proxy_hbox), streaming_proxy_port_entry, FALSE, FALSE, 0); + + streaming_proxy_auth_use = gtk_check_button_new_with_label(_("Use authentication")); + gtk_widget_set_sensitive(streaming_proxy_auth_use, flac_cfg.stream.use_proxy); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(streaming_proxy_auth_use), flac_cfg.stream.proxy_use_auth); + gtk_signal_connect(GTK_OBJECT(streaming_proxy_auth_use), "clicked", GTK_SIGNAL_FUNC(proxy_auth_use_cb), NULL); + gtk_box_pack_start(GTK_BOX(streaming_proxy_vbox), streaming_proxy_auth_use, FALSE, FALSE, 0); + + streaming_proxy_auth_hbox = gtk_hbox_new(FALSE, 5); + gtk_widget_set_sensitive(streaming_proxy_auth_hbox, flac_cfg.stream.use_proxy && flac_cfg.stream.proxy_use_auth); + gtk_box_pack_start(GTK_BOX(streaming_proxy_vbox), streaming_proxy_auth_hbox, FALSE, FALSE, 0); + + streaming_proxy_auth_user_label = gtk_label_new(_("Username:")); + gtk_box_pack_start(GTK_BOX(streaming_proxy_auth_hbox), streaming_proxy_auth_user_label, FALSE, FALSE, 0); + + streaming_proxy_auth_user_entry = gtk_entry_new(); + if(flac_cfg.stream.proxy_user) + gtk_entry_set_text(GTK_ENTRY(streaming_proxy_auth_user_entry), flac_cfg.stream.proxy_user); + gtk_box_pack_start(GTK_BOX(streaming_proxy_auth_hbox), streaming_proxy_auth_user_entry, TRUE, TRUE, 0); + + streaming_proxy_auth_pass_label = gtk_label_new(_("Password:")); + gtk_box_pack_start(GTK_BOX(streaming_proxy_auth_hbox), streaming_proxy_auth_pass_label, FALSE, FALSE, 0); + + streaming_proxy_auth_pass_entry = gtk_entry_new(); + if(flac_cfg.stream.proxy_pass) + gtk_entry_set_text(GTK_ENTRY(streaming_proxy_auth_pass_entry), flac_cfg.stream.proxy_pass); + gtk_entry_set_visibility(GTK_ENTRY(streaming_proxy_auth_pass_entry), FALSE); + gtk_box_pack_start(GTK_BOX(streaming_proxy_auth_hbox), streaming_proxy_auth_pass_entry, TRUE, TRUE, 0); + + + /* + * Save to disk config. + */ + streaming_save_frame = gtk_frame_new(_("Save stream to disk:")); + gtk_container_set_border_width(GTK_CONTAINER(streaming_save_frame), 5); + gtk_box_pack_start(GTK_BOX(streaming_vbox), streaming_save_frame, FALSE, FALSE, 0); + + streaming_save_vbox = gtk_vbox_new(FALSE, 5); + gtk_container_set_border_width(GTK_CONTAINER(streaming_save_vbox), 5); + gtk_container_add(GTK_CONTAINER(streaming_save_frame), streaming_save_vbox); + + streaming_save_use = gtk_check_button_new_with_label(_("Save stream to disk")); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(streaming_save_use), flac_cfg.stream.save_http_stream); + gtk_signal_connect(GTK_OBJECT(streaming_save_use), "clicked", GTK_SIGNAL_FUNC(streaming_save_use_cb), NULL); + gtk_box_pack_start(GTK_BOX(streaming_save_vbox), streaming_save_use, FALSE, FALSE, 0); + + streaming_save_hbox = gtk_hbox_new(FALSE, 5); + gtk_widget_set_sensitive(streaming_save_hbox, flac_cfg.stream.save_http_stream); + gtk_box_pack_start(GTK_BOX(streaming_save_vbox), streaming_save_hbox, FALSE, FALSE, 0); + + streaming_save_label = gtk_label_new(_("Path:")); + gtk_box_pack_start(GTK_BOX(streaming_save_hbox), streaming_save_label, FALSE, FALSE, 0); + + streaming_save_entry = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(streaming_save_entry), flac_cfg.stream.save_http_path); + gtk_box_pack_start(GTK_BOX(streaming_save_hbox), streaming_save_entry, TRUE, TRUE, 0); + + streaming_save_browse = gtk_button_new_with_label(_("Browse")); + gtk_signal_connect(GTK_OBJECT(streaming_save_browse), "clicked", GTK_SIGNAL_FUNC(streaming_save_browse_cb), NULL); + gtk_box_pack_start(GTK_BOX(streaming_save_hbox), streaming_save_browse, FALSE, FALSE, 0); + +#ifdef FLAC_ICECAST + streaming_cast_frame = gtk_frame_new(_("SHOUT/Icecast:")); + gtk_container_set_border_width(GTK_CONTAINER(streaming_cast_frame), 5); + gtk_box_pack_start(GTK_BOX(streaming_vbox), streaming_cast_frame, FALSE, FALSE, 0); + + streaming_cast_vbox = gtk_vbox_new(5, FALSE); + gtk_container_add(GTK_CONTAINER(streaming_cast_frame), streaming_cast_vbox); + + streaming_cast_title = gtk_check_button_new_with_label(_("Enable SHOUT/Icecast title streaming")); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(streaming_cast_title), flac_cfg.stream.cast_title_streaming); + gtk_box_pack_start(GTK_BOX(streaming_cast_vbox), streaming_cast_title, FALSE, FALSE, 0); + + streaming_udp_title = gtk_check_button_new_with_label(_("Enable Icecast Metadata UDP Channel")); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(streaming_udp_title), flac_cfg.stream.use_udp_channel); + gtk_box_pack_start(GTK_BOX(streaming_cast_vbox), streaming_udp_title, FALSE, FALSE, 0); +#endif + + gtk_notebook_append_page(GTK_NOTEBOOK(notebook), streaming_vbox, gtk_label_new(_("Streaming"))); + /* Buttons */ bbox = gtk_hbutton_box_new(); diff --git a/src/plugin_xmms/configure.h b/src/plugin_xmms/configure.h index 7bf55116..24e0e508 100644 --- a/src/plugin_xmms/configure.h +++ b/src/plugin_xmms/configure.h @@ -31,6 +31,21 @@ typedef struct { gchar *user_char_set; } title; + struct { + gint http_buffer_size; + gint http_prebuffer; + gboolean use_proxy; + gchar *proxy_host; + gint proxy_port; + gboolean proxy_use_auth; + gchar *proxy_user; + gchar *proxy_pass; + gboolean save_http_stream; + gchar *save_http_path; + gboolean cast_title_streaming; + gboolean use_udp_channel; + } stream; + struct { struct { gboolean enable; diff --git a/src/plugin_xmms/http.c b/src/plugin_xmms/http.c new file mode 100644 index 00000000..d8d7de97 --- /dev/null +++ b/src/plugin_xmms/http.c @@ -0,0 +1,887 @@ +/* XMMS - Cross-platform multimedia player + * Copyright (C) 1998-2000 Peter Alm, Mikael Alm, Olle Hallnas, Thomas Nilsson and 4Front Technologies + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +/* modified for FLAC support by Steven Richman (2003) */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "configure.h" +#include "plugin_common/locale_hack.h" +#include "FLAC/format.h" +#include "plugin.h" + +#define min(x,y) ((x)<(y)?(x):(y)) +#define min3(x,y,z) (min(x,y)<(z)?min(x,y):(z)) +#define min4(x,y,z,w) (min3(x,y,z)<(w)?min3(x,y,z):(w)) + +static gchar *icy_name = NULL; +static gint icy_metaint = 0; + +extern InputPlugin flac_ip; + +#undef DEBUG_UDP + +/* Static udp channel functions */ +static int udp_establish_listener (gint *sock); +static int udp_check_for_data(gint sock); + +static char *flac_http_get_title(char *url); + +static gboolean prebuffering, going, eof = FALSE; +static gint sock, rd_index, wr_index, buffer_length, prebuffer_length; +static guint64 buffer_read = 0; +static gchar *buffer; +static guint64 offset; +static pthread_t thread; +static GtkWidget *error_dialog = NULL; + +static FILE *output_file = NULL; + +#define BASE64_LENGTH(len) (4 * (((len) + 2) / 3)) + +/* Encode the string S of length LENGTH to base64 format and place it + to STORE. STORE will be 0-terminated, and must point to a writable + buffer of at least 1+BASE64_LENGTH(length) bytes. */ +static void base64_encode (const gchar *s, gchar *store, gint length) +{ + /* Conversion table. */ + static gchar tbl[64] = { + 'A','B','C','D','E','F','G','H', + 'I','J','K','L','M','N','O','P', + 'Q','R','S','T','U','V','W','X', + 'Y','Z','a','b','c','d','e','f', + 'g','h','i','j','k','l','m','n', + 'o','p','q','r','s','t','u','v', + 'w','x','y','z','0','1','2','3', + '4','5','6','7','8','9','+','/' + }; + gint i; + guchar *p = (guchar *)store; + + /* Transform the 3x8 bits to 4x6 bits, as required by base64. */ + for (i = 0; i < length; i += 3) + { + *p++ = tbl[s[0] >> 2]; + *p++ = tbl[((s[0] & 3) << 4) + (s[1] >> 4)]; + *p++ = tbl[((s[1] & 0xf) << 2) + (s[2] >> 6)]; + *p++ = tbl[s[2] & 0x3f]; + s += 3; + } + /* Pad the result if necessary... */ + if (i == length + 1) + *(p - 1) = '='; + else if (i == length + 2) + *(p - 1) = *(p - 2) = '='; + /* ...and zero-terminate it. */ + *p = '\0'; +} + +/* Create the authentication header contents for the `Basic' scheme. + This is done by encoding the string `USER:PASS' in base64 and + prepending `HEADER: Basic ' to it. */ +static gchar *basic_authentication_encode (const gchar *user, const gchar *passwd, const gchar *header) +{ + gchar *t1, *t2, *res; + gint len1 = strlen (user) + 1 + strlen (passwd); + gint len2 = BASE64_LENGTH (len1); + + t1 = g_strdup_printf("%s:%s", user, passwd); + t2 = g_malloc0(len2 + 1); + base64_encode (t1, t2, len1); + res = g_strdup_printf("%s: Basic %s\r\n", header, t2); + g_free(t2); + g_free(t1); + + return res; +} + +static void parse_url(const gchar * url, gchar ** user, gchar ** pass, gchar ** host, int *port, gchar ** filename) +{ + gchar *h, *p, *pt, *f, *temp, *ptr; + + temp = g_strdup(url); + ptr = temp; + + if (!strncasecmp("http://", ptr, 7)) + ptr += 7; + h = strchr(ptr, '@'); + f = strchr(ptr, '/'); + if (h != NULL && (!f || h < f)) + { + *h = '\0'; + p = strchr(ptr, ':'); + if (p != NULL && p < h) + { + *p = '\0'; + p++; + *pass = g_strdup(p); + } + else + *pass = NULL; + *user = g_strdup(ptr); + h++; + ptr = h; + } + else + { + *user = NULL; + *pass = NULL; + h = ptr; + } + pt = strchr(ptr, ':'); + if (pt != NULL && (f == NULL || pt < f)) + { + *pt = '\0'; + *port = atoi(pt + 1); + } + else + { + if (f) + *f = '\0'; + *port = 80; + } + *host = g_strdup(h); + + if (f) + *filename = g_strdup(f + 1); + else + *filename = NULL; + g_free(temp); +} + +void flac_http_close(void) +{ + going = FALSE; + + pthread_join(thread, NULL); + g_free(icy_name); + icy_name = NULL; +} + + +static gint http_used(void) +{ + if (wr_index >= rd_index) + return wr_index - rd_index; + return buffer_length - (rd_index - wr_index); +} + +static gint http_free(void) +{ + if (rd_index > wr_index) + return (rd_index - wr_index) - 1; + return (buffer_length - (wr_index - rd_index)) - 1; +} + +static void http_wait_for_data(gint bytes) +{ + while ((prebuffering || http_used() < bytes) && !eof && going) + xmms_usleep(10000); +} + +static void show_error_message(gchar *error) +{ + if(!error_dialog) + { + GDK_THREADS_ENTER(); + error_dialog = xmms_show_message(_("Error"), error, _("Ok"), FALSE, + NULL, NULL); + gtk_signal_connect(GTK_OBJECT(error_dialog), + "destroy", + GTK_SIGNAL_FUNC(gtk_widget_destroyed), + &error_dialog); + GDK_THREADS_LEAVE(); + } +} + +int flac_http_read(gpointer data, gint length) +{ + gint len, cnt, off = 0, meta_len, meta_off = 0, i; + gchar *meta_data, **tags, *temp, *title; + if (length > buffer_length) { + length = buffer_length; + } + + http_wait_for_data(length); + + if (!going) + return 0; + len = min(http_used(), length); + + while (len && http_used()) + { + if ((flac_cfg.stream.cast_title_streaming) && (icy_metaint > 0) && (buffer_read % icy_metaint) == 0 && (buffer_read > 0)) + { + meta_len = *((guchar *) buffer + rd_index) * 16; + rd_index = (rd_index + 1) % buffer_length; + if (meta_len > 0) + { + http_wait_for_data(meta_len); + meta_data = g_malloc0(meta_len); + if (http_used() >= meta_len) + { + while (meta_len) + { + cnt = min(meta_len, buffer_length - rd_index); + memcpy(meta_data + meta_off, buffer + rd_index, cnt); + rd_index = (rd_index + cnt) % buffer_length; + meta_len -= cnt; + meta_off += cnt; + } + tags = g_strsplit(meta_data, "';", 0); + + for (i = 0; tags[i]; i++) + { + if (!strncasecmp(tags[i], "StreamTitle=", 12)) + { + temp = g_strdup(tags[i] + 13); + title = g_strdup_printf("%s (%s)", temp, icy_name); + set_track_info(title, -1); + g_free(title); + g_free(temp); + } + + } + g_strfreev(tags); + + } + g_free(meta_data); + } + if (!http_used()) + http_wait_for_data(length - off); + cnt = min3(len, buffer_length - rd_index, http_used()); + } + else if ((icy_metaint > 0) && (flac_cfg.stream.cast_title_streaming)) + cnt = min4(len, buffer_length - rd_index, http_used(), icy_metaint - (gint) (buffer_read % icy_metaint)); + else + cnt = min3(len, buffer_length - rd_index, http_used()); + if (output_file) + fwrite(buffer + rd_index, 1, cnt, output_file); + + memcpy((gchar *)data + off, buffer + rd_index, cnt); + rd_index = (rd_index + cnt) % buffer_length; + buffer_read += cnt; + len -= cnt; + off += cnt; + } + if (!off) { + fprintf(stderr, "returning zero\n"); + } + return off; +} + +static gboolean http_check_for_data(void) +{ + + fd_set set; + struct timeval tv; + gint ret; + + tv.tv_sec = 0; + tv.tv_usec = 20000; + FD_ZERO(&set); + FD_SET(sock, &set); + ret = select(sock + 1, &set, NULL, NULL, &tv); + if (ret > 0) + return TRUE; + return FALSE; +} + +gint flac_http_read_line(gchar * buf, gint size) +{ + gint i = 0; + + while (going && i < size - 1) + { + if (http_check_for_data()) + { + if (read(sock, buf + i, 1) <= 0) + return -1; + if (buf[i] == '\n') + break; + if (buf[i] != '\r') + i++; + } + } + if (!going) + return -1; + buf[i] = '\0'; + return i; +} + +/* returns the file descriptor of the socket, or -1 on error */ +static int http_connect (gchar *url_, gboolean head, guint64 offset) +{ + gchar line[1024], *user, *pass, *host, *filename, + *status, *url, *temp, *file; + gchar *chost; + gint cnt, error, err_len, port, cport; + gboolean redirect; + int udp_sock = 0; + fd_set set; + struct hostent *hp; + struct sockaddr_in address; + struct timeval tv; + + url = g_strdup (url_); + + do + { + redirect=FALSE; + + g_strstrip(url); + + parse_url(url, &user, &pass, &host, &port, &filename); + + if ((!filename || !*filename) && url[strlen(url) - 1] != '/') + temp = g_strconcat(url, "/", NULL); + else + temp = g_strdup(url); + g_free(url); + url = temp; + + chost = flac_cfg.stream.use_proxy ? flac_cfg.stream.proxy_host : host; + cport = flac_cfg.stream.use_proxy ? flac_cfg.stream.proxy_port : port; + + sock = socket(AF_INET, SOCK_STREAM, 0); + fcntl(sock, F_SETFL, O_NONBLOCK); + address.sin_family = AF_INET; + + status = g_strdup_printf(_("LOOKING UP %s"), chost); + flac_ip.set_info_text(status); + g_free(status); + + if (!(hp = gethostbyname(chost))) + { + status = g_strdup_printf(_("Couldn't look up host %s"), chost); + show_error_message(status); + g_free(status); + + flac_ip.set_info_text(NULL); + eof = TRUE; + } + + if (!eof) + { + memcpy(&address.sin_addr.s_addr, *(hp->h_addr_list), sizeof (address.sin_addr.s_addr)); + address.sin_port = (gint) g_htons(cport); + + status = g_strdup_printf(_("CONNECTING TO %s:%d"), chost, cport); + flac_ip.set_info_text(status); + g_free(status); + if (connect(sock, (struct sockaddr *) &address, sizeof (struct sockaddr_in)) == -1) + { + if (errno != EINPROGRESS) + { + status = g_strdup_printf(_("Couldn't connect to host %s"), chost); + show_error_message(status); + g_free(status); + + flac_ip.set_info_text(NULL); + eof = TRUE; + } + } + while (going) + { + tv.tv_sec = 0; + tv.tv_usec = 10000; + FD_ZERO(&set); + FD_SET(sock, &set); + if (select(sock + 1, NULL, &set, NULL, &tv) > 0) + { + err_len = sizeof (error); + getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &err_len); + if (error) + { + status = g_strdup_printf(_("Couldn't connect to host %s"), + chost); + show_error_message(status); + g_free(status); + + flac_ip.set_info_text(NULL); + eof = TRUE; + + } + break; + } + } + if (!eof) + { + gchar *auth = NULL, *proxy_auth = NULL; + gchar udpspace[30]; + int udp_port; + + if (flac_cfg.stream.use_udp_channel) + { + udp_port = udp_establish_listener (&udp_sock); + if (udp_port > 0) + sprintf (udpspace, "x-audiocast-udpport: %d\r\n", udp_port); + else + udp_sock = 0; + } + + if(user && pass) + auth = basic_authentication_encode(user, pass, "Authorization"); + + if (flac_cfg.stream.use_proxy) + { + file = g_strdup(url); + if(flac_cfg.stream.proxy_use_auth && flac_cfg.stream.proxy_user && flac_cfg.stream.proxy_pass) + { + proxy_auth = basic_authentication_encode(flac_cfg.stream.proxy_user, + flac_cfg.stream.proxy_pass, + "Proxy-Authorization"); + } + } + else + file = g_strconcat("/", filename, NULL); + + temp = g_strdup_printf("GET %s HTTP/1.0\r\n" + "Host: %s\r\n" + "User-Agent: %s/%s\r\n" + "%s%s%s%s", + file, host, "Reference FLAC Player", FLAC__VERSION_STRING, + proxy_auth ? proxy_auth : "", auth ? auth : "", + flac_cfg.stream.cast_title_streaming ? "Icy-MetaData:1\r\n" : "", + flac_cfg.stream.use_udp_channel ? udpspace : ""); + if (offset && !head) { + gchar *temp_dead = temp; + temp = g_strconcat ("%sRange: %ll-\r\n", temp, offset); + fprintf (stderr, "%s", temp); + g_free (temp_dead); + } + + g_free(file); + if(proxy_auth) + g_free(proxy_auth); + if(auth) + g_free(auth); + write(sock, temp, strlen(temp)); + write(sock, "\r\n", 2); + g_free(temp); + flac_ip.set_info_text(_("CONNECTED: WAITING FOR REPLY")); + while (going && !eof) + { + if (http_check_for_data()) + { + if (flac_http_read_line(line, 1024)) + { + status = strchr(line, ' '); + if (status) + { + if (status[1] == '2') + break; + else if(status[1] == '3' && status[2] == '0' && status[3] == '2') + { + while(going) + { + if(http_check_for_data()) + { + if((cnt = flac_http_read_line(line, 1024)) != -1) + { + if(!cnt) + break; + if(!strncmp(line, "Location:", 9)) + { + g_free(url); + url = g_strdup(line+10); + } + } + else + { + eof=TRUE; + flac_ip.set_info_text(NULL); + break; + } + } + } + redirect=TRUE; + break; + } + else + { + status = g_strdup_printf(_("Couldn't connect to host %s\nServer reported: %s"), chost, status); + show_error_message(status); + g_free(status); + break; + } + } + } + else + { + eof = TRUE; + flac_ip.set_info_text(NULL); + } + } + } + + while (going && !redirect) + { + if (http_check_for_data()) + { + if ((cnt = flac_http_read_line(line, 1024)) != -1) + { + if (!cnt) + break; + if (!strncmp(line, "icy-name:", 9)) + icy_name = g_strdup(line + 9); + else if (!strncmp(line, "x-audiocast-name:", 17)) + icy_name = g_strdup(line + 17); + if (!strncmp(line, "icy-metaint:", 12)) + icy_metaint = atoi(line + 12); + if (!strncmp(line, "x-audiocast-udpport:", 20)) { +#ifdef DEBUG_UDP + fprintf (stderr, "Server wants udp messages on port %d\n", atoi (line + 20)); +#endif + /*udp_serverport = atoi (line + 20);*/ + } + + } + else + { + eof = TRUE; + flac_ip.set_info_text(NULL); + break; + } + } + } + } + } + + if(redirect) + { + if (output_file) + { + fclose(output_file); + output_file = NULL; + } + close(sock); + } + + g_free(user); + g_free(pass); + g_free(host); + g_free(filename); + } while(redirect); + + g_free(url); + return eof ? -1 : sock; +} + +static void *http_buffer_loop(void *arg) +{ + gchar *status, *url, *temp, *file; + gint cnt, written; + int udp_sock = 0; + + url = (gchar *) arg; + sock = http_connect (url, false, offset); + + if (sock >= 0 && flac_cfg.stream.save_http_stream) { + gchar *output_name; + file = flac_http_get_title(url); + output_name = file; + if (!strncasecmp(output_name, "http://", 7)) + output_name += 7; + temp = strrchr(output_name, '.'); + if (temp && (!strcasecmp(temp, ".fla") || !strcasecmp(temp, ".flac"))) + *temp = '\0'; + + while ((temp = strchr(output_name, '/'))) + *temp = '_'; + output_name = g_strdup_printf("%s/%s.flac", flac_cfg.stream.save_http_path, output_name); + + g_free(file); + + output_file = fopen(output_name, "wb"); + g_free(output_name); + } + + while (going) + { + + if (!http_used() && !flac_ip.output->buffer_playing()) + prebuffering = TRUE; + if (http_free() > 0 && !eof) + { + if (http_check_for_data()) + { + cnt = min(http_free(), buffer_length - wr_index); + if (cnt > 1024) + cnt = 1024; + written = read(sock, buffer + wr_index, cnt); + if (written <= 0) + { + eof = TRUE; + if (prebuffering) + { + prebuffering = FALSE; + + flac_ip.set_info_text(NULL); + } + + } + else + wr_index = (wr_index + written) % buffer_length; + } + + if (prebuffering) + { + if (http_used() > prebuffer_length) + { + prebuffering = FALSE; + flac_ip.set_info_text(NULL); + } + else + { + status = g_strdup_printf(_("PRE-BUFFERING: %dKB/%dKB"), http_used() / 1024, prebuffer_length / 1024); + flac_ip.set_info_text(status); + g_free(status); + } + + } + } + else + xmms_usleep(10000); + + if (flac_cfg.stream.use_udp_channel && udp_sock != 0) + if (udp_check_for_data(udp_sock) < 0) + { + close(udp_sock); + udp_sock = 0; + } + } + if (output_file) + { + fclose(output_file); + output_file = NULL; + } + if (sock >= 0) { + close(sock); + } + if (udp_sock != 0) + close(udp_sock); + + g_free(buffer); + g_free(url); + + pthread_exit(NULL); +} + +int flac_http_open(gchar * _url, guint64 _offset) +{ + gchar *url; + + url = g_strdup(_url); + + rd_index = 0; + wr_index = 0; + buffer_length = flac_cfg.stream.http_buffer_size * 1024; + prebuffer_length = (buffer_length * flac_cfg.stream.http_prebuffer) / 100; + buffer_read = 0; + icy_metaint = 0; + prebuffering = TRUE; + going = TRUE; + eof = FALSE; + buffer = g_malloc(buffer_length); + offset = _offset; + + pthread_create(&thread, NULL, http_buffer_loop, url); + + return 0; +} + +char *flac_http_get_title(char *url) +{ + if (icy_name) + return g_strdup(icy_name); + if (g_basename(url) && strlen(g_basename(url)) > 0) + return g_strdup(g_basename(url)); + return g_strdup(url); +} + +/* Start UDP Channel specific stuff */ + +/* Find a good local udp port and bind udp_sock to it, return the port */ +static int udp_establish_listener(int *sock) +{ + struct sockaddr_in sin; + socklen_t sinlen = sizeof (struct sockaddr_in); + +#ifdef DEBUG_UDP + fprintf (stderr,"Establishing udp listener\n"); +#endif + + if ((*sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) + { + g_log(NULL, G_LOG_LEVEL_CRITICAL, + "udp_establish_listener(): unable to create socket"); + return -1; + } + + memset(&sin, 0, sinlen); + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = g_htonl(INADDR_ANY); + + if (bind(*sock, (struct sockaddr *)&sin, sinlen) < 0) + { + g_log(NULL, G_LOG_LEVEL_CRITICAL, + "udp_establish_listener(): Failed to bind socket to localhost: %s", strerror(errno)); + close(*sock); + return -1; + } + if (fcntl(*sock, F_SETFL, O_NONBLOCK) < 0) + { + g_log(NULL, G_LOG_LEVEL_CRITICAL, + "udp_establish_listener(): Failed to set flags: %s", strerror(errno)); + close(*sock); + return -1; + } + + memset(&sin, 0, sinlen); + if (getsockname(*sock, (struct sockaddr *)&sin, &sinlen) < 0) + { + g_log(NULL, G_LOG_LEVEL_CRITICAL, + "udp_establish_listener(): Failed to retrieve socket info: %s", strerror(errno)); + close(*sock); + return -1; + } + +#ifdef DEBUG_UDP + fprintf (stderr,"Listening on local %s:%d\n", inet_ntoa(sin.sin_addr), g_ntohs(sin.sin_port)); +#endif + + return g_ntohs(sin.sin_port); +} + +static int udp_check_for_data(int sock) +{ + char buf[1025], **lines; + char *valptr; + gchar *title; + gint len, i; + struct sockaddr_in from; + socklen_t fromlen; + + fromlen = sizeof(struct sockaddr_in); + + if ((len = recvfrom(sock, buf, 1024, 0, (struct sockaddr *)&from, &fromlen)) < 0) + { + if (errno != EAGAIN) + { + g_log(NULL, G_LOG_LEVEL_CRITICAL, + "udp_read_data(): Error reading from socket: %s", strerror(errno)); + return -1; + } + return 0; + } + buf[len] = '\0'; +#ifdef DEBUG_UDP + fprintf (stderr,"Received: [%s]\n", buf); +#endif + lines = g_strsplit(buf, "\n", 0); + if (!lines) + return 0; + + for (i = 0; lines[i]; i++) + { + while ((lines[i][strlen(lines[i]) - 1] == '\n') || + (lines[i][strlen(lines[i]) - 1] == '\r')) + lines[i][strlen(lines[i]) - 1] = '\0'; + + valptr = strchr(lines[i], ':'); + + if (!valptr) + continue; + else + valptr++; + + g_strstrip(valptr); + if (!strlen(valptr)) + continue; + + if (strstr(lines[i], "x-audiocast-streamtitle") != NULL) + { + title = g_strdup_printf ("%s (%s)", valptr, icy_name); + if (going) + set_track_info(title, -1); + g_free (title); + } + +#if 0 + else if (strstr(lines[i], "x-audiocast-streamlength") != NULL) + { + if (atoi(valptr) != -1) + set_track_info(NULL, atoi(valptr)); + } +#endif + + else if (strstr(lines[i], "x-audiocast-streammsg") != NULL) + { + /* set_track_info(title, -1); */ +/* xmms_show_message(_("Message"), valptr, _("Ok"), */ +/* FALSE, NULL, NULL); */ + g_message("Stream_message: %s", valptr); + } + +#if 0 + /* Use this to direct your webbrowser.. yeah right.. */ + else if (strstr(lines[i], "x-audiocast-streamurl") != NULL) + { + if (lasturl && g_strcmp (valptr, lasturl)) + { + c_message (stderr, "Song URL: %s\n", valptr); + g_free (lasturl); + lasturl = g_strdup (valptr); + } + } +#endif + else if (strstr(lines[i], "x-audiocast-udpseqnr:") != NULL) + { + gchar obuf[60]; + sprintf(obuf, "x-audiocast-ack: %ld \r\n", atol(valptr)); + if (sendto(sock, obuf, strlen(obuf), 0, (struct sockaddr *) &from, fromlen) < 0) + { + g_log(NULL, G_LOG_LEVEL_WARNING, + "udp_check_for_data(): Unable to send ack to server: %s", strerror(errno)); + } +#ifdef DEBUG_UDP + else + fprintf(stderr,"Sent ack: %s", obuf); + fprintf (stderr,"Remote: %s:%d\n", inet_ntoa(from.sin_addr), g_ntohs(from.sin_port)); +#endif + } + } + g_strfreev(lines); + return 0; +} diff --git a/src/plugin_xmms/http.h b/src/plugin_xmms/http.h new file mode 100644 index 00000000..7cb9d4dc --- /dev/null +++ b/src/plugin_xmms/http.h @@ -0,0 +1,26 @@ +/* libxmms-flac - XMMS FLAC input plugin + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +#ifndef __HTTP_H__ +#define __HTTP_H__ + +extern int flac_http_open(gchar * url, guint64 offset); +extern void flac_http_close(void); +extern int flac_http_read(gpointer data, gint length); + + +#endif diff --git a/src/plugin_xmms/plugin.c b/src/plugin_xmms/plugin.c index bdb1e4a3..d95ae37f 100644 --- a/src/plugin_xmms/plugin.c +++ b/src/plugin_xmms/plugin.c @@ -21,6 +21,9 @@ #include #include #include +#include +#include +#include #include #include @@ -41,6 +44,7 @@ #include "share/replaygain_synthesis.h" #include "configure.h" #include "charset.h" +#include "http.h" #include "tag.h" #ifdef min @@ -76,6 +80,35 @@ typedef struct { DitherContext dither_context; } file_info_struct; +typedef FLAC__StreamDecoderWriteStatus (*WriteCallback) (const void *decoder, const FLAC__Frame *frame, const FLAC__int32 * const buffer[], void *client_data); +typedef void (*MetadataCallback) (const void *decoder, const FLAC__StreamMetadata *metadata, void *client_data); +typedef void (*ErrorCallback) (const void *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data); + +typedef struct { + FLAC__bool seekable; + void* (*new_decoder) (void); + FLAC__bool (*set_md5_checking) (void *decoder, FLAC__bool value); + FLAC__bool (*set_source) (void *decoder, const char* source); + FLAC__bool (*set_metadata_ignore_all) (void *decoder); + FLAC__bool (*set_metadata_respond) (void *decoder, FLAC__MetadataType type); + FLAC__bool (*set_write_callback) (void *decoder, WriteCallback value); + FLAC__bool (*set_metadata_callback) (void *decoder, MetadataCallback value); + FLAC__bool (*set_error_callback) (void *decoder, ErrorCallback value); + FLAC__bool (*set_client_data) (void *decoder, void *value); + FLAC__bool (*decoder_init) (void *decoder); + void (*safe_decoder_finish) (void *decoder); + void (*safe_decoder_delete) (void *decoder); + FLAC__bool (*process_until_end_of_metadata) (void *decoder); + FLAC__bool (*process_single) (void *decoder); + FLAC__bool (*is_eof) (void *decoder); +} decoder_funcs_t; + +#define NUM_DECODER_TYPES 2 +typedef enum { + DECODER_FILE, + DECODER_HTTP +} decoder_t; + static void FLAC_XMMS__init(); static int FLAC_XMMS__is_our_file(char *filename); static void FLAC_XMMS__play_file(char *filename); @@ -87,12 +120,16 @@ static void FLAC_XMMS__cleanup(); static void FLAC_XMMS__get_song_info(char *filename, char **title, int *length); static void *play_loop_(void *arg); -static FLAC__bool safe_decoder_init_(const char *filename, FLAC__FileDecoder *decoder); -static void safe_decoder_finish_(FLAC__FileDecoder *decoder); -static void safe_decoder_delete_(FLAC__FileDecoder *decoder); -static FLAC__StreamDecoderWriteStatus write_callback_(const FLAC__FileDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 * const buffer[], void *client_data); -static void metadata_callback_(const FLAC__FileDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data); -static void error_callback_(const FLAC__FileDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data); + +static FLAC__bool safe_decoder_init_(const char *filename, void **decoderp, decoder_funcs_t const ** fnsp); +static void file_decoder_safe_decoder_finish_(void *decoder); +static void file_decoder_safe_decoder_delete_(void *decoder); +static FLAC__StreamDecoderWriteStatus write_callback_(const void *decoder, const FLAC__Frame *frame, const FLAC__int32 * const buffer[], void *client_data); +static void metadata_callback_(const void *decoder, const FLAC__StreamMetadata *metadata, void *client_data); +static void error_callback_(const void *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data); + +static void init_decoder_func_tables(); +static decoder_t source_to_decoder_type (const char *source); InputPlugin flac_ip = { @@ -127,7 +164,7 @@ InputPlugin flac_ip = static FLAC__byte sample_buffer_[SAMPLE_BUFFER_SIZE]; static unsigned sample_buffer_first_, sample_buffer_last_; -static FLAC__FileDecoder *decoder_ = 0; +static void *decoder_ = 0; static file_info_struct file_info_; static pthread_t decode_thread_; static FLAC__bool audio_error_ = false; @@ -138,6 +175,11 @@ static FLAC__bool is_big_endian_host_; #define BITRATE_HIST_SIZE 50 static unsigned bitrate_history_[BITRATE_HIST_SIZE]; +/* A table of sets of decoder functions, indexed by decoder_t */ +static const decoder_funcs_t* DECODER_FUNCS[NUM_DECODER_TYPES]; + +static decoder_funcs_t const * decoder_func_table_; + InputPlugin *get_iplugin_info() { @@ -145,11 +187,36 @@ InputPlugin *get_iplugin_info() return &flac_ip; } +void set_track_info(const char* title, int length_in_msec) +{ + if (file_info_.is_playing) { + flac_ip.set_info((char*) title, length_in_msec, file_info_.sample_rate * file_info_.channels * file_info_.bits_per_sample, file_info_.sample_rate, file_info_.channels); + } +} + +static gchar* homedir() +{ + gchar *result; + char *env_home = getenv("HOME"); + if (env_home) { + result = g_strdup (env_home); + } else { + uid_t uid = getuid(); + struct passwd *pwent; + do { + pwent = getpwent(); + } while (pwent && pwent->pw_uid != uid); + result = pwent ? g_strdup (pwent->pw_dir) : NULL; + endpwent(); + } + return result; +} + void FLAC_XMMS__init() { ConfigFile *cfg; FLAC__uint32 test = 1; - + is_big_endian_host_ = (*((FLAC__byte*)(&test)))? false : true; flac_cfg.title.tag_override = FALSE; @@ -190,7 +257,29 @@ void FLAC_XMMS__init() if(!xmms_cfg_read_int(cfg, "flac", "output.resolution.replaygain.bps_out", &flac_cfg.output.resolution.replaygain.bps_out)) flac_cfg.output.resolution.replaygain.bps_out = 16; - decoder_ = FLAC__file_decoder_new(); + /* stream */ + + xmms_cfg_read_int(cfg, "flac", "stream.http_buffer_size", &flac_cfg.stream.http_buffer_size); + xmms_cfg_read_int(cfg, "flac", "stream.http_prebuffer", &flac_cfg.stream.http_prebuffer); + xmms_cfg_read_boolean(cfg, "flac", "stream.use_proxy", &flac_cfg.stream.use_proxy); + xmms_cfg_read_string(cfg, "flac", "stream.proxy_host", &flac_cfg.stream.proxy_host); + xmms_cfg_read_int(cfg, "flac", "stream.proxy_port", &flac_cfg.stream.proxy_port); + xmms_cfg_read_boolean(cfg, "flac", "stream.proxy_use_auth", &flac_cfg.stream.proxy_use_auth); + xmms_cfg_read_string(cfg, "flac", "stream.proxy_user", &flac_cfg.stream.proxy_user); + xmms_cfg_read_string(cfg, "flac", "stream.proxy_pass", &flac_cfg.stream.proxy_pass); + xmms_cfg_read_boolean(cfg, "flac", "stream.save_http_stream", &flac_cfg.stream.save_http_stream); + if (!xmms_cfg_read_string(cfg, "flac", "stream.save_http_path", &flac_cfg.stream.save_http_path) || + ! *flac_cfg.stream.save_http_path) { + if (flac_cfg.stream.save_http_path) + g_free (flac_cfg.stream.save_http_path); + flac_cfg.stream.save_http_path = homedir(); + } + xmms_cfg_read_boolean(cfg, "flac", "stream.cast_title_streaming", &flac_cfg.stream.cast_title_streaming); + xmms_cfg_read_boolean(cfg, "flac", "stream.use_udp_channel", &flac_cfg.stream.use_udp_channel); + + init_decoder_func_tables(); + decoder_func_table_ = DECODER_FUNCS [DECODER_FILE]; + decoder_ = decoder_func_table_ -> new_decoder(); xmms_cfg_free(cfg); } @@ -218,14 +307,16 @@ void FLAC_XMMS__play_file(char *filename) file_info_.play_thread_open = false; file_info_.has_replaygain = false; - if(0 == (f = fopen(filename, "r"))) - return; - fclose(f); + if (source_to_decoder_type (filename) == DECODER_FILE) { + if(0 == (f = fopen(filename, "r"))) + return; + fclose(f); + } if(decoder_ == 0) return; - if(!safe_decoder_init_(filename, decoder_)) + if(!safe_decoder_init_(filename, &decoder_, &decoder_func_table_)) return; if(file_info_.has_replaygain && flac_cfg.output.replaygain.enable) { @@ -240,7 +331,7 @@ void FLAC_XMMS__play_file(char *filename) else { /*@@@ need some error here like wa2: MessageBox(mod_.hMainWindow, "ERROR: plugin can only handle 8/16-bit samples\n", "ERROR: plugin can only handle 8/16-bit samples", 0); */ fprintf(stderr, "libxmms-flac: can't handle %d bit output\n", flac_cfg.output.resolution.replaygain.bps_out); - safe_decoder_finish_(decoder_); + decoder_func_table_ -> safe_decoder_finish(decoder_); return; } } @@ -256,7 +347,7 @@ void FLAC_XMMS__play_file(char *filename) else { /*@@@ need some error here like wa2: MessageBox(mod_.hMainWindow, "ERROR: plugin can only handle 8/16-bit samples\n", "ERROR: plugin can only handle 8/16-bit samples", 0); */ fprintf(stderr, "libxmms-flac: can't handle %d bit output\n", file_info_.bits_per_sample); - safe_decoder_finish_(decoder_); + decoder_func_table_ -> safe_decoder_finish(decoder_); return; } } @@ -265,7 +356,7 @@ void FLAC_XMMS__play_file(char *filename) if(flac_ip.output->open_audio(file_info_.sample_format, file_info_.sample_rate, file_info_.channels) == 0) { audio_error_ = true; - safe_decoder_finish_(decoder_); + decoder_func_table_ -> safe_decoder_finish(decoder_); return; } @@ -286,7 +377,7 @@ void FLAC_XMMS__stop() pthread_join(decode_thread_, NULL); } flac_ip.output->close_audio(); - safe_decoder_finish_(decoder_); + decoder_func_table_ -> safe_decoder_finish (decoder_); } } @@ -297,11 +388,13 @@ void FLAC_XMMS__pause(short p) void FLAC_XMMS__seek(int time) { - file_info_.seek_to_in_sec = time; - file_info_.eof = false; + if (decoder_func_table_->seekable) { + file_info_.seek_to_in_sec = time; + file_info_.eof = false; - while(file_info_.seek_to_in_sec != -1) - xmms_usleep(10000); + while(file_info_.seek_to_in_sec != -1) + xmms_usleep(10000); + } } int FLAC_XMMS__get_time() @@ -316,7 +409,7 @@ int FLAC_XMMS__get_time() void FLAC_XMMS__cleanup() { - safe_decoder_delete_(decoder_); + decoder_func_table_ -> safe_decoder_delete(decoder_); decoder_ = 0; } @@ -330,9 +423,13 @@ void FLAC_XMMS__get_song_info(char *filename, char **title, int *length_in_msec) if(!FLAC__metadata_get_streaminfo(filename, &streaminfo)) { /* @@@ how to report the error? */ if(title) { - static const char *errtitle = "Invalid FLAC File: "; - *title = g_malloc(strlen(errtitle) + 1 + strlen(filename) + 1 + 1); - sprintf(*title, "%s\"%s\"", errtitle, filename); + if (source_to_decoder_type (filename) == DECODER_FILE) { + static const char *errtitle = "Invalid FLAC File: "; + *title = g_malloc(strlen(errtitle) + 1 + strlen(filename) + 1 + 1); + sprintf(*title, "%s\"%s\"", errtitle, filename); + } else { + *title = NULL; + } } if(length_in_msec) *length_in_msec = -1; @@ -363,11 +460,11 @@ void *play_loop_(void *arg) unsigned s; s = sample_buffer_last_ - sample_buffer_first_; - if(FLAC__file_decoder_get_state(decoder_) == FLAC__FILE_DECODER_END_OF_FILE) { + if(decoder_func_table_ -> is_eof(decoder_)) { file_info_.eof = true; break; } - else if(!FLAC__file_decoder_process_single(decoder_)) { + else if (!decoder_func_table_ -> process_single(decoder_)) { /*@@@ this should probably be a dialog */ fprintf(stderr, "libxmms-flac: READ ERROR processing frame\n"); file_info_.eof = true; @@ -375,7 +472,7 @@ void *play_loop_(void *arg) } blocksize = sample_buffer_last_ - sample_buffer_first_ - s; decode_position_frame_last = decode_position_frame; - if(!FLAC__file_decoder_get_decode_position(decoder_, &decode_position_frame)) + if(!decoder_func_table_->seekable || !FLAC__file_decoder_get_decode_position(decoder_, &decode_position_frame)) decode_position_frame = 0; } if(sample_buffer_last_ - sample_buffer_first_ > 0) { @@ -414,7 +511,7 @@ void *play_loop_(void *arg) } else xmms_usleep(10000); - if(file_info_.seek_to_in_sec != -1) { + if(decoder_func_table_->seekable && file_info_.seek_to_in_sec != -1) { const double distance = (double)file_info_.seek_to_in_sec * 1000.0 / (double)file_info_.length_in_msec; unsigned target_sample = (unsigned)(distance * (double)file_info_.total_samples); if(FLAC__file_decoder_seek_absolute(decoder_, (FLAC__uint64)target_sample)) { @@ -437,7 +534,7 @@ void *play_loop_(void *arg) } } - safe_decoder_finish_(decoder_); + decoder_func_table_ -> safe_decoder_finish(decoder_); /* are these two calls necessary? */ flac_ip.output->buffer_free(); @@ -449,46 +546,185 @@ void *play_loop_(void *arg) return 0; /* to silence the compiler warning about not returning a value */ } -FLAC__bool safe_decoder_init_(const char *filename, FLAC__FileDecoder *decoder) +/*********** File decoder functions */ + +static FLAC__bool file_decoder_init (void *decoder) { - if(decoder == 0) + return FLAC__file_decoder_init( (FLAC__FileDecoder*) decoder) == FLAC__FILE_DECODER_OK; +} + +static void file_decoder_safe_decoder_finish_(void *decoder) +{ + if(decoder && FLAC__file_decoder_get_state((FLAC__FileDecoder *) decoder) != FLAC__FILE_DECODER_UNINITIALIZED) + FLAC__file_decoder_finish((FLAC__FileDecoder *) decoder); +} + +static void file_decoder_safe_decoder_delete_(void *decoder) +{ + if(decoder) { + file_decoder_safe_decoder_finish_(decoder); + FLAC__file_decoder_delete( (FLAC__FileDecoder *) decoder); + } +} + +static FLAC__bool file_decoder_is_eof(void *decoder) +{ + return FLAC__file_decoder_get_state((FLAC__FileDecoder *) decoder) == FLAC__FILE_DECODER_END_OF_FILE; +} + +static const decoder_funcs_t FILE_DECODER_FUNCTIONS = { + true, + (void* (*) (void)) FLAC__file_decoder_new, + (FLAC__bool (*) (void *, FLAC__bool)) FLAC__file_decoder_set_md5_checking, + (FLAC__bool (*) (void *, const char*)) FLAC__file_decoder_set_filename, + (FLAC__bool (*) (void *)) FLAC__file_decoder_set_metadata_ignore_all, + (FLAC__bool (*) (void *, FLAC__MetadataType)) FLAC__file_decoder_set_metadata_respond, + (FLAC__bool (*) (void *, WriteCallback)) FLAC__file_decoder_set_write_callback, + (FLAC__bool (*) (void *, MetadataCallback)) FLAC__file_decoder_set_metadata_callback, + (FLAC__bool (*) (void *, ErrorCallback)) FLAC__file_decoder_set_error_callback, + (FLAC__bool (*) (void *, void *)) FLAC__file_decoder_set_client_data, + (FLAC__bool (*) (void *)) file_decoder_init, + (void (*) (void *)) file_decoder_safe_decoder_finish_, + (void (*) (void *)) file_decoder_safe_decoder_delete_, + (FLAC__bool (*) (void *)) FLAC__file_decoder_process_until_end_of_metadata, + (FLAC__bool (*) (void *)) FLAC__file_decoder_process_single, + file_decoder_is_eof +}; + +/*********** HTTP decoder functions */ + +static gchar *url_; + +static FLAC__bool http_decoder_set_md5_checking (void *decoder, FLAC__bool value) +{ + (void) value; + // operation unsupported + return FLAC__stream_decoder_get_state ((const FLAC__StreamDecoder *) decoder) == + FLAC__STREAM_DECODER_UNINITIALIZED; +} + +static FLAC__bool http_decoder_set_url (void *decoder, const char* url) +{ + (void) decoder; + url_ = g_strdup (url); + return true; +} + +static FLAC__StreamDecoderReadStatus http_decoder_read_callback (const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], unsigned *bytes, void *client_data) +{ + (void) decoder; + (void) client_data; + *bytes = flac_http_read (buffer, *bytes); + return *bytes ? FLAC__STREAM_DECODER_READ_STATUS_CONTINUE : FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM; +} + +static FLAC__bool http_decoder_init (void *decoder) +{ + flac_http_open (url_, 0); + g_free (url_); + FLAC__stream_decoder_set_read_callback (decoder, http_decoder_read_callback); + return FLAC__stream_decoder_init( (FLAC__StreamDecoder*) decoder) == FLAC__STREAM_DECODER_SEARCH_FOR_METADATA; +} + +static void http_decoder_safe_decoder_finish_(void *decoder) +{ + if(decoder && FLAC__stream_decoder_get_state((FLAC__StreamDecoder *) decoder) != FLAC__STREAM_DECODER_UNINITIALIZED) { + FLAC__stream_decoder_finish((FLAC__StreamDecoder *) decoder); + flac_http_close(); + } +} + +static void http_decoder_safe_decoder_delete_(void *decoder) +{ + if(decoder) { + http_decoder_safe_decoder_finish_(decoder); + FLAC__stream_decoder_delete( (FLAC__StreamDecoder *) decoder); + } +} + +static FLAC__bool http_decoder_is_eof(void *decoder) +{ + return FLAC__stream_decoder_get_state((FLAC__StreamDecoder *) decoder) == FLAC__STREAM_DECODER_END_OF_STREAM; +} + +static const decoder_funcs_t HTTP_DECODER_FUNCTIONS = { + false, + (void* (*) (void)) FLAC__stream_decoder_new, + http_decoder_set_md5_checking, + (FLAC__bool (*) (void *, const char*)) http_decoder_set_url, + (FLAC__bool (*) (void *)) FLAC__stream_decoder_set_metadata_ignore_all, + (FLAC__bool (*) (void *, FLAC__MetadataType)) FLAC__stream_decoder_set_metadata_respond, + (FLAC__bool (*) (void *, WriteCallback)) FLAC__stream_decoder_set_write_callback, + (FLAC__bool (*) (void *, MetadataCallback)) FLAC__stream_decoder_set_metadata_callback, + (FLAC__bool (*) (void *, ErrorCallback)) FLAC__stream_decoder_set_error_callback, + (FLAC__bool (*) (void *, void *)) FLAC__stream_decoder_set_client_data, + (FLAC__bool (*) (void *)) http_decoder_init, + (void (*) (void *)) http_decoder_safe_decoder_finish_, + (void (*) (void *)) http_decoder_safe_decoder_delete_, + (FLAC__bool (*) (void *)) FLAC__stream_decoder_process_until_end_of_metadata, + (FLAC__bool (*) (void *)) FLAC__stream_decoder_process_single, + http_decoder_is_eof +}; + +static decoder_funcs_t const *decoder_func_table_; + +static void init_decoder_func_tables() +{ + DECODER_FUNCS [DECODER_FILE] = & FILE_DECODER_FUNCTIONS; + DECODER_FUNCS [DECODER_HTTP] = & HTTP_DECODER_FUNCTIONS; +} + +static decoder_t source_to_decoder_type (const char *source) +{ + return strncasecmp(source, "http://", 7) ? DECODER_FILE : DECODER_HTTP; +} + +static void change_decoder_if_needed (decoder_t new_decoder_type, void **decoderp, decoder_funcs_t const ** fntabp) +{ + const decoder_funcs_t *new_fn_table = DECODER_FUNCS [new_decoder_type]; + if (*fntabp != new_fn_table) { + (*fntabp)->safe_decoder_delete(*decoderp); + *fntabp = new_fn_table; + *decoderp = new_fn_table -> new_decoder(); + } +} + +FLAC__bool safe_decoder_init_(const char *filename, void **decoderp, decoder_funcs_t const ** fntabp) +{ + if(decoderp == 0 || *decoderp == 0) return false; - safe_decoder_finish_(decoder); + (*fntabp)->safe_decoder_finish(*decoderp); - FLAC__file_decoder_set_md5_checking(decoder, false); - FLAC__file_decoder_set_filename(decoder, filename); - FLAC__file_decoder_set_metadata_ignore_all(decoder); - FLAC__file_decoder_set_metadata_respond(decoder, FLAC__METADATA_TYPE_STREAMINFO); - FLAC__file_decoder_set_metadata_respond(decoder, FLAC__METADATA_TYPE_VORBIS_COMMENT); - FLAC__file_decoder_set_write_callback(decoder, write_callback_); - FLAC__file_decoder_set_metadata_callback(decoder, metadata_callback_); - FLAC__file_decoder_set_error_callback(decoder, error_callback_); - FLAC__file_decoder_set_client_data(decoder, &file_info_); - if(FLAC__file_decoder_init(decoder) != FLAC__FILE_DECODER_OK) - return false; + change_decoder_if_needed(source_to_decoder_type(filename), decoderp, fntabp); - if(!FLAC__file_decoder_process_until_end_of_metadata(decoder)) - return false; + { + decoder_funcs_t const *fntab = *fntabp; + void *decoder = *decoderp; + + decoder = *decoderp; + fntab = *fntabp; + + fntab -> set_md5_checking(decoder, false); + fntab -> set_source(decoder, filename); + fntab -> set_metadata_ignore_all(decoder); + fntab -> set_metadata_respond(decoder, FLAC__METADATA_TYPE_STREAMINFO); + fntab -> set_metadata_respond(decoder, FLAC__METADATA_TYPE_VORBIS_COMMENT); + fntab -> set_write_callback(decoder, write_callback_); + fntab -> set_metadata_callback(decoder, metadata_callback_); + fntab -> set_error_callback(decoder, error_callback_); + fntab -> set_client_data(decoder, &file_info_); + if(!fntab -> decoder_init(decoder)) + return false; + + if(!fntab -> process_until_end_of_metadata(decoder)) + return false; + } return true; } -void safe_decoder_finish_(FLAC__FileDecoder *decoder) -{ - if(decoder && FLAC__file_decoder_get_state(decoder) != FLAC__FILE_DECODER_UNINITIALIZED) - FLAC__file_decoder_finish(decoder); -} - -void safe_decoder_delete_(FLAC__FileDecoder *decoder) -{ - if(decoder) { - safe_decoder_finish_(decoder); - FLAC__file_decoder_delete(decoder); - } -} - -FLAC__StreamDecoderWriteStatus write_callback_(const FLAC__FileDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 * const buffer[], void *client_data) +FLAC__StreamDecoderWriteStatus write_callback_(const void *decoder, const FLAC__Frame *frame, const FLAC__int32 * const buffer[], void *client_data) { file_info_struct *file_info = (file_info_struct *)client_data; const unsigned channels = file_info->channels, wide_samples = frame->header.blocksize; @@ -548,7 +784,7 @@ FLAC__StreamDecoderWriteStatus write_callback_(const FLAC__FileDecoder *decoder, return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; } -void metadata_callback_(const FLAC__FileDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data) +void metadata_callback_(const void *decoder, const FLAC__StreamMetadata *metadata, void *client_data) { file_info_struct *file_info = (file_info_struct *)client_data; (void)decoder; @@ -569,7 +805,7 @@ void metadata_callback_(const FLAC__FileDecoder *decoder, const FLAC__StreamMeta } } -void error_callback_(const FLAC__FileDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data) +void error_callback_(const void *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data) { file_info_struct *file_info = (file_info_struct *)client_data; (void)decoder; diff --git a/src/plugin_xmms/plugin.h b/src/plugin_xmms/plugin.h new file mode 100644 index 00000000..e900df7e --- /dev/null +++ b/src/plugin_xmms/plugin.h @@ -0,0 +1,24 @@ +/* libxmms-flac - XMMS FLAC input plugin + * Copyright (C) 2004 Josh Coalson + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +#ifndef FLAC__PLUGIN_XMMS__PLUGIN_H +#define FLAC__PLUGIN_XMMS__PLUGIN_H + +void set_track_info(const char* title, int length_in_msec); + +#endif