/* * Copyright (c) 2002, 2003, 2004 Jean-Yves Lefort * * 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. */ #include "config.h" #include #include #include #include #include #include #ifdef WITH_LOCAL_METADATA #include #endif #include "streamtuner.h" /*** cpp *********************************************************************/ #define POINTER_TO_STRING(ptr) ((ptr) ? (ptr) : "") /*** type definitions ********************************************************/ typedef struct { STStream stream; char *pathname; /* full pathname, in the fs locale */ char *filename; /* filename only, converted to UTF-8 */ char *title; char *artist; char *album; char *year; char *genre; char *comment; char *duration; int bitrate; int samplerate; int channels; } LocalStream; enum { FIELD_PATHNAME, FIELD_FILENAME, FIELD_TITLE, FIELD_ARTIST, FIELD_ALBUM, FIELD_YEAR, FIELD_GENRE, FIELD_COMMENT, FIELD_DURATION, FIELD_BITRATE, FIELD_SAMPLERATE, FIELD_CHANNELS, FIELD_AUDIO /* meta field (bitrate, samplerate and channels) */ }; /*** variables ***************************************************************/ static STPlugin *local_plugin = NULL; static STHandler *local_handler = NULL; /*** function declarations ***************************************************/ static LocalStream *stream_new_cb (gpointer data); static void stream_field_get_cb (LocalStream *stream, STHandlerField *field, GValue *value, gpointer data); static void stream_field_set_cb (LocalStream *stream, STHandlerField *field, const GValue *value, gpointer data); static void stream_stock_field_get_cb (LocalStream *stream, STHandlerStockField stock_field, GValue *value, gpointer data); static gboolean stream_modify_cb (LocalStream *stream, GSList *fields, GSList *values, gpointer data, GError **err); static gboolean stream_delete_cb (LocalStream *stream, gpointer data, GError **err); static void stream_free_cb (LocalStream *stream, gpointer data); static gboolean stream_rename (LocalStream *stream, const GValue *new_filename, GError **err); static gboolean stream_tune_in_multiple_cb (GSList *streams, gpointer data, GError **err); static gboolean stream_browse_cb (LocalStream *stream, gpointer data, GError **err); static gboolean reload_cb (STCategory *category, GNode **categories, GList **streams, gpointer data, GError **err); static gboolean reload_categories (const char *music_dir, GNode *root, GError **err); static gboolean reload_streams (const char *music_dir, STCategory *category, GList **streams, GError **err); #ifdef WITH_LOCAL_METADATA static void metadata_read (LocalStream *stream); static gboolean metadata_write (LocalStream *stream, GSList *fields, GSList *values, GError **err); #endif /* WITH_LOCAL_METADATA */ static void init_handler (void); static gboolean check_api_version (GError **err); /*** implementation **********************************************************/ static LocalStream * stream_new_cb (gpointer data) { return g_new0(LocalStream, 1); } static void stream_field_get_cb (LocalStream *stream, STHandlerField *field, GValue *value, gpointer data) { switch (field->id) { case FIELD_PATHNAME: g_value_set_string(value, stream->pathname); break; case FIELD_FILENAME: g_value_set_string(value, stream->filename); break; case FIELD_TITLE: g_value_set_string(value, stream->title); break; case FIELD_ARTIST: g_value_set_string(value, stream->artist); break; case FIELD_ALBUM: g_value_set_string(value, stream->album); break; case FIELD_YEAR: g_value_set_string(value, stream->year); break; case FIELD_GENRE: g_value_set_string(value, stream->genre); break; case FIELD_COMMENT: g_value_set_string(value, stream->comment); break; case FIELD_DURATION: g_value_set_string(value, stream->duration); break; case FIELD_BITRATE: g_value_set_int(value, stream->bitrate); break; case FIELD_SAMPLERATE: g_value_set_int(value, stream->samplerate); break; case FIELD_CHANNELS: g_value_set_int(value, stream->channels); break; case FIELD_AUDIO: g_value_take_string(value, st_format_audio_properties(stream->bitrate, stream->samplerate, stream->channels)); break; default: g_assert_not_reached(); } } static void stream_field_set_cb (LocalStream *stream, STHandlerField *field, const GValue *value, gpointer data) { switch (field->id) { case FIELD_PATHNAME: stream->pathname = g_value_dup_string(value); break; case FIELD_FILENAME: stream->filename = g_value_dup_string(value); break; case FIELD_TITLE: stream->title = g_value_dup_string(value); break; case FIELD_ARTIST: stream->artist = g_value_dup_string(value); break; case FIELD_ALBUM: stream->album = g_value_dup_string(value); break; case FIELD_YEAR: stream->year = g_value_dup_string(value); break; case FIELD_GENRE: stream->genre = g_value_dup_string(value); break; case FIELD_COMMENT: stream->comment = g_value_dup_string(value); break; case FIELD_DURATION: stream->duration = g_value_dup_string(value); break; case FIELD_BITRATE: stream->bitrate = g_value_get_int(value); break; case FIELD_SAMPLERATE: stream->samplerate = g_value_get_int(value); break; case FIELD_CHANNELS: stream->channels = g_value_get_int(value); break; default: g_assert_not_reached(); } } static void stream_stock_field_get_cb (LocalStream *stream, STHandlerStockField stock_field, GValue *value, gpointer data) { switch (stock_field) { case ST_HANDLER_STOCK_FIELD_NAME: { char *name; if (stream->artist && stream->title) name = g_strdup_printf("%s - %s", stream->artist, stream->title); else if (stream->title) name = g_strdup(stream->title); else name = g_strdup(stream->filename); g_value_set_string(value, name); g_free(name); break; } case ST_HANDLER_STOCK_FIELD_GENRE: g_value_set_string(value, stream->genre); break; case ST_HANDLER_STOCK_FIELD_DESCRIPTION: g_value_set_string(value, stream->comment); break; case ST_HANDLER_STOCK_FIELD_HOMEPAGE: /* nop */ break; case ST_HANDLER_STOCK_FIELD_URI_LIST: { char *uri; GError *err = NULL; uri = g_filename_to_uri(stream->pathname, NULL, &err); if (uri) { GValueArray *value_array; GValue uri_value = { 0, }; value_array = g_value_array_new(1); g_value_init(&uri_value, G_TYPE_STRING); g_value_take_string(&uri_value, uri); g_value_array_append(value_array, &uri_value); g_value_unset(&uri_value); g_value_take_boxed(value, value_array); } else { st_handler_notice(local_handler, _("%s: unable to convert filename to URI: %s"), stream->pathname, err->message); g_error_free(err); } break; } } } static gboolean stream_modify_cb (LocalStream *stream, GSList *fields, GSList *values, gpointer data, GError **err) { GSList *f; GSList *v; gboolean modify_file = FALSE; for (f = fields, v = values; f && v; f = f->next, v = v->next) { STHandlerField *field = f->data; const GValue *value = v->data; switch (field->id) { case FIELD_FILENAME: if (! stream_rename(stream, value, err)) return FALSE; break; case FIELD_TITLE: case FIELD_ARTIST: case FIELD_ALBUM: case FIELD_YEAR: case FIELD_GENRE: case FIELD_COMMENT: modify_file = TRUE; break; default: g_assert_not_reached(); } } if (modify_file) { #ifdef WITH_LOCAL_METADATA if (! metadata_write(stream, fields, values, err)) return FALSE; #else g_set_error(err, 0, 0, _("metadata support is disabled")); return FALSE; #endif /* WITH_LOCAL_METADATA */ } return TRUE; } static gboolean stream_delete_cb (LocalStream *stream, gpointer data, GError **err) { if (unlink(stream->pathname) < 0) { g_set_error(err, 0, 0, "%s", g_strerror(errno)); return FALSE; } else return TRUE; } static void stream_free_cb (LocalStream *stream, gpointer data) { g_free(stream->pathname); g_free(stream->filename); g_free(stream->title); g_free(stream->artist); g_free(stream->album); g_free(stream->year); g_free(stream->genre); g_free(stream->comment); g_free(stream->duration); st_stream_free((STStream *) stream); } static gboolean stream_rename (LocalStream *stream, const GValue *new_filename, GError **err) { GError *tmp_err = NULL; char *filename; char *directory; char *new_pathname; g_return_val_if_fail(stream != NULL, FALSE); g_return_val_if_fail(G_IS_VALUE(new_filename), FALSE); filename = g_filename_from_utf8(g_value_get_string(new_filename), -1, NULL, NULL, &tmp_err); if (! filename) { g_set_error(err, 0, 0, _("unable to convert filename from UTF-8 encoding: %s"), tmp_err->message); g_error_free(tmp_err); return FALSE; } directory = g_path_get_dirname(stream->pathname); new_pathname = g_build_filename(directory, filename, NULL); g_free(directory); if (g_file_test(new_pathname, G_FILE_TEST_EXISTS)) { g_set_error(err, 0, 0, _("target file already exists")); g_free(filename); g_free(new_pathname); return FALSE; } if (rename(stream->pathname, new_pathname) < 0) { g_set_error(err, 0, 0, "%s", g_strerror(errno)); g_free(filename); g_free(new_pathname); return FALSE; } /* success */ stream->pathname = new_pathname; stream->filename = g_value_dup_string(new_filename); return TRUE; } static gboolean stream_tune_in_multiple_cb (GSList *streams, gpointer data, GError **err) { char *m3uname; GSList *filenames = NULL; GSList *l; gboolean status; /* create a list of filenames from STREAMS */ for (l = streams; l; l = l->next) { LocalStream *stream = l->data; filenames = g_slist_append(filenames, stream->pathname); } /* write the .m3u */ m3uname = st_m3u_mktemp("streamtuner.local.XXXXXX", filenames, err); g_slist_free(filenames); if (! m3uname) return FALSE; /* open the .m3u */ status = st_action_run("play-m3u", m3uname, err); g_free(m3uname); return status; } static gboolean stream_browse_cb (LocalStream *stream, gpointer data, GError **err) { char *url; char *s; gboolean status; if (stream->album) url = g_strconcat("http://www.allmusic.com/cg/amg.dll?p=amg&opt1=2&sql=", stream->album, NULL); else if (stream->title) url = g_strconcat("http://www.allmusic.com/cg/amg.dll?p=amg&opt1=3&sql=", stream->title, NULL); else if (stream->artist) url = g_strconcat("http://www.allmusic.com/cg/amg.dll?p=amg&opt1=1&sql=", stream->artist, NULL); else { g_set_error(err, 0, 0, _("file has no album, title or artist information")); return FALSE; } /* allmusic.com needs this */ for (s = url; *s; s++) if (*s == ' ') *s = '|'; status = st_action_run("view-web", url, err); g_free(url); return status; } static gboolean reload_cb (STCategory *category, GNode **categories, GList **streams, gpointer data, GError **err) { char *music_dir; gboolean status; *categories = g_node_new(NULL); music_dir = st_settings_get_music_dir(); if (! music_dir) { g_set_error(err, 0, 0, _("you must set your music folder in the Preferences")); return FALSE; } status = reload_categories(music_dir, *categories, err) && reload_streams(music_dir, category, streams, err); g_free(music_dir); return status; } static gboolean reload_categories (const char *music_dir, GNode *root, GError **err) { GDir *dir; char *dirname; const char *filename; gboolean status = TRUE; GError *tmp_err = NULL; g_return_val_if_fail(music_dir != NULL, FALSE); g_return_val_if_fail(root != NULL, FALSE); dirname = root->data ? g_build_filename(music_dir, ((STCategory *) root->data)->url_postfix, NULL) : g_strdup(music_dir); dir = g_dir_open(dirname, 0, &tmp_err); if (! dir) { g_set_error(err, 0, 0, _("unable to open directory %s: %s"), dirname, tmp_err->message); g_error_free(tmp_err); status = FALSE; goto end; } while ((filename = g_dir_read_name(dir))) { GNode *node; char *pathname; if (st_is_aborted()) { status = FALSE; goto end; } if (filename[0] == '.') continue; pathname = g_build_filename(dirname, filename, NULL); if (g_file_test(pathname, G_FILE_TEST_IS_DIR)) { STCategory *category; category = st_category_new(); category->name = root->data ? g_build_filename(((STCategory *) root->data)->url_postfix, filename, NULL) : g_strdup(filename); category->label = g_filename_to_utf8(filename, -1, NULL, NULL, &tmp_err); if (! category->label) { st_handler_notice(local_handler, _("%s: unable to convert directory name to UTF-8 encoding: %s"), pathname, tmp_err->message); g_clear_error(&tmp_err); } category->url_postfix = g_strdup(category->name); node = g_node_append_data(root, category); if (! reload_categories(music_dir, node, err)) { status = FALSE; goto end; } } g_free(pathname); } end: if (dir) g_dir_close(dir); g_free(dirname); return status; } static gboolean reload_streams (const char *music_dir, STCategory *category, GList **streams, GError **err) { GDir *dir; char *dirname; const char *filename; gboolean status = TRUE; GError *tmp_err = NULL; g_return_val_if_fail(music_dir != NULL, FALSE); g_return_val_if_fail(category != NULL, FALSE); g_return_val_if_fail(streams != NULL, FALSE); dirname = category->url_postfix ? g_build_filename(music_dir, category->url_postfix, NULL) : g_strdup(music_dir); dir = g_dir_open(dirname, 0, &tmp_err); if (! dir) { g_set_error(err, 0, 0, _("unable to open directory %s: %s"), dirname, tmp_err->message); g_error_free(tmp_err); status = FALSE; goto end; } while ((filename = g_dir_read_name(dir))) { LocalStream *stream; char *extension; if (st_is_aborted()) { status = FALSE; goto end; } if (filename[0] == '.') continue; extension = strrchr(filename, '.'); if (! (extension++ && (! g_ascii_strcasecmp(extension, "mp3") || ! g_ascii_strcasecmp(extension, "ogg") || ! g_ascii_strcasecmp(extension, "m3u") || ! g_ascii_strcasecmp(extension, "pls")))) continue; /* unhandled */ stream = stream_new_cb(NULL); stream->pathname = g_build_filename(dirname, filename, NULL); ((STStream *) stream)->name = g_strdup(filename); stream->filename = g_filename_to_utf8(filename, -1, NULL, NULL, &tmp_err); if (! stream->filename) { st_handler_notice(local_handler, _("%s: unable to convert filename to UTF-8 encoding: %s"), stream->pathname, tmp_err->message); g_clear_error(&tmp_err); } #ifdef WITH_LOCAL_METADATA metadata_read(stream); #endif *streams = g_list_append(*streams, stream); } end: if (dir) g_dir_close(dir); g_free(dirname); return status; } #ifdef WITH_LOCAL_METADATA static void metadata_read (LocalStream *stream) { TagLib_File *file; TagLib_Tag *tag; const TagLib_AudioProperties *audio_properties; g_return_if_fail(stream != NULL); file = taglib_file_new(stream->pathname); if (! file) { st_handler_notice(local_handler, _("unable to open %s"), stream->pathname); return; } tag = taglib_file_tag(file); if (tag) { char *title; char *artist; char *album; unsigned int year; char *genre; char *comment; title = taglib_tag_title(tag); g_return_if_fail(title != NULL); artist = taglib_tag_artist(tag); g_return_if_fail(artist != NULL); album = taglib_tag_album(tag); g_return_if_fail(album != NULL); year = taglib_tag_year(tag); genre = taglib_tag_genre(tag); g_return_if_fail(genre != NULL); comment = taglib_tag_comment(tag); g_return_if_fail(comment != NULL); if (*title) stream->title = g_strdup(title); if (*artist) stream->artist = g_strdup(artist); if (*album) stream->album = g_strdup(album); if (year != 0) stream->year = g_strdup_printf("%u", year); if (*genre) stream->genre = g_strdup(genre); if (*comment) stream->comment = g_strdup(comment); taglib_tag_free_strings(); } else st_handler_notice(local_handler, _("%s has no tag"), stream->pathname); audio_properties = taglib_file_audioproperties(file); if (audio_properties) { int length; length = taglib_audioproperties_length(audio_properties); if (length != 0) stream->duration = g_strdup_printf("%02u:%02u", length / 60, length % 60); stream->bitrate = taglib_audioproperties_bitrate(audio_properties); stream->samplerate = taglib_audioproperties_samplerate(audio_properties); stream->channels = taglib_audioproperties_channels(audio_properties); } else st_handler_notice(local_handler, _("%s has no audio properties"), stream->pathname); taglib_file_free(file); } static gboolean metadata_write (LocalStream *stream, GSList *fields, GSList *values, GError **err) { TagLib_File *file; TagLib_Tag *tag; gboolean status; g_return_val_if_fail(stream != NULL, FALSE); file = taglib_file_new(stream->pathname); if (! file) { g_set_error(err, 0, 0, _("unable to open file")); return FALSE; } tag = taglib_file_tag(file); if (tag) { GSList *f; GSList *v; for (f = fields, v = values; f && v; f = f->next, v = v->next) { STHandlerField *field = f->data; const GValue *value = v->data; const char *str = g_value_get_string(value); char **ptr = NULL; switch (field->id) { case FIELD_TITLE: taglib_tag_set_title(tag, POINTER_TO_STRING(str)); ptr = &stream->title; break; case FIELD_ARTIST: taglib_tag_set_artist(tag, POINTER_TO_STRING(str)); ptr = &stream->artist; break; case FIELD_ALBUM: taglib_tag_set_album(tag, POINTER_TO_STRING(str)); ptr = &stream->album; break; case FIELD_YEAR: { int year = str ? atoi(str) : 0; taglib_tag_set_year(tag, year); ptr = &stream->year; } break; case FIELD_GENRE: taglib_tag_set_genre(tag, POINTER_TO_STRING(str)); ptr = &stream->genre; break; case FIELD_COMMENT: taglib_tag_set_comment(tag, POINTER_TO_STRING(str)); ptr = &stream->comment; break; } if (ptr) { g_free(*ptr); *ptr = g_strdup(str); } } status = taglib_file_save(file); if (! status) g_set_error(err, 0, 0, _("unable to save file")); } else { status = FALSE; g_set_error(err, 0, 0, _("the tag structure is missing")); } taglib_file_free(file); return status; } #endif /* WITH_LOCAL_METADATA */ static void init_handler (void) { GNode *stock_categories; STCategory *category; STHandlerField *field; local_handler = st_handler_new_from_plugin(local_plugin); st_handler_set_description(local_handler, _("Local Music Collection")); stock_categories = g_node_new(NULL); category = st_category_new(); category->name = "__main"; category->label = _("Root"); g_node_append_data(stock_categories, category); st_handler_set_stock_categories(local_handler, stock_categories); st_handler_set_flags(local_handler, ST_HANDLER_CONFIRM_DELETION); st_handler_bind(local_handler, ST_HANDLER_EVENT_RELOAD, reload_cb, NULL); st_handler_bind(local_handler, ST_HANDLER_EVENT_STREAM_NEW, stream_new_cb, NULL); st_handler_bind(local_handler, ST_HANDLER_EVENT_STREAM_FIELD_GET, stream_field_get_cb, NULL); st_handler_bind(local_handler, ST_HANDLER_EVENT_STREAM_FIELD_SET, stream_field_set_cb, NULL); st_handler_bind(local_handler, ST_HANDLER_EVENT_STREAM_STOCK_FIELD_GET, stream_stock_field_get_cb, NULL); st_handler_bind(local_handler, ST_HANDLER_EVENT_STREAM_MODIFY, stream_modify_cb, NULL); st_handler_bind(local_handler, ST_HANDLER_EVENT_STREAM_DELETE, stream_delete_cb, NULL); st_handler_bind(local_handler, ST_HANDLER_EVENT_STREAM_FREE, stream_free_cb, NULL); st_handler_bind(local_handler, ST_HANDLER_EVENT_STREAM_TUNE_IN_MULTIPLE, stream_tune_in_multiple_cb, NULL); st_handler_bind(local_handler, ST_HANDLER_EVENT_STREAM_BROWSE, stream_browse_cb, NULL); st_handler_add_field(local_handler, st_handler_field_new(FIELD_PATHNAME, _("Pathname"), G_TYPE_STRING, 0)); field = st_handler_field_new(FIELD_FILENAME, _("Filename"), G_TYPE_STRING, ST_HANDLER_FIELD_VISIBLE | ST_HANDLER_FIELD_EDITABLE); st_handler_field_set_description(field, _("The song filename")); st_handler_add_field(local_handler, field); field = st_handler_field_new(FIELD_TITLE, _("Title"), G_TYPE_STRING, ST_HANDLER_FIELD_VISIBLE | ST_HANDLER_FIELD_EDITABLE); st_handler_field_set_description(field, _("The song title")); st_handler_add_field(local_handler, field); field = st_handler_field_new(FIELD_ARTIST, _("Artist"), G_TYPE_STRING, ST_HANDLER_FIELD_VISIBLE | ST_HANDLER_FIELD_EDITABLE); st_handler_field_set_description(field, _("The performing artist")); st_handler_add_field(local_handler, field); field = st_handler_field_new(FIELD_ALBUM, _("Album"), G_TYPE_STRING, ST_HANDLER_FIELD_VISIBLE | ST_HANDLER_FIELD_EDITABLE); st_handler_field_set_description(field, _("The album the song was released on")); st_handler_add_field(local_handler, field); field = st_handler_field_new(FIELD_YEAR, _("Year"), G_TYPE_STRING, ST_HANDLER_FIELD_VISIBLE | ST_HANDLER_FIELD_EDITABLE); st_handler_field_set_description(field, _("The song release year")); st_handler_add_field(local_handler, field); field = st_handler_field_new(FIELD_GENRE, _("Genre"), G_TYPE_STRING, ST_HANDLER_FIELD_VISIBLE | ST_HANDLER_FIELD_EDITABLE | ST_HANDLER_FIELD_START_HIDDEN); st_handler_field_set_description(field, _("The song genre")); st_handler_add_field(local_handler, field); field = st_handler_field_new(FIELD_COMMENT, _("Comment"), G_TYPE_STRING, ST_HANDLER_FIELD_VISIBLE | ST_HANDLER_FIELD_EDITABLE | ST_HANDLER_FIELD_START_HIDDEN); st_handler_field_set_description(field, _("The song comment")); st_handler_add_field(local_handler, field); field = st_handler_field_new(FIELD_DURATION, _("Duration"), G_TYPE_STRING, ST_HANDLER_FIELD_VISIBLE); st_handler_field_set_description(field, _("The song duration")); st_handler_add_field(local_handler, field); field = st_handler_field_new(FIELD_AUDIO, _("Audio"), G_TYPE_STRING, ST_HANDLER_FIELD_VISIBLE | ST_HANDLER_FIELD_START_HIDDEN | ST_HANDLER_FIELD_VOLATILE); st_handler_field_set_description(field, _("The song audio properties")); st_handler_add_field(local_handler, field); /* invisible fields */ st_handler_add_field(local_handler, st_handler_field_new(FIELD_BITRATE, _("Bitrate"), G_TYPE_INT, 0)); st_handler_add_field(local_handler, st_handler_field_new(FIELD_SAMPLERATE, _("Sample rate"), G_TYPE_INT, 0)); st_handler_add_field(local_handler, st_handler_field_new(FIELD_CHANNELS, _("Channels"), G_TYPE_INT, 0)); st_handlers_add(local_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; local_plugin = plugin; st_plugin_set_name(plugin, "local"); st_plugin_set_label(plugin, _("Local")); pixbuf = st_pixbuf_new_from_file(UIDIR "/local.png"); if (pixbuf) { st_plugin_set_icon_from_pixbuf(plugin, pixbuf); g_object_unref(pixbuf); } return TRUE; } G_MODULE_EXPORT gboolean plugin_init (GError **err) { if (! check_api_version(err)) return FALSE; init_handler(); st_action_register("play-m3u", _("Listen to a .m3u file"), "xmms %q"); st_action_register("view-web", _("Open a web page"), "epiphany %q"); return TRUE; }