/* Copyright 2006 Maxime Petazzoni * * gmpc-lyrics : A lyrics fetcher for GMpc * * 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., 675 Mass Ave, Cambridge, MA 02139, USA. * */ #include "gmpc-lyrics.h" #define LYRICS_FROM "\n\nLyric from " static GtkWidget *lyrics_pref_vbox = NULL; static GtkWidget *lyrics_pref_table = NULL; static xmlGenericErrorFunc handler = NULL; int lyrics_get_enabled(); void lyrics_set_enabled(int enabled); /* Playlist window row reference */ int fetch_lyric(mpd_Song *song, MetaDataType type, char **path); int fetch_lyric_loop(mpd_Song *song, gchar **lyrics, int preferred_api, int exact); void xml_error_func(void * ctx, const char * msg, ...); void setup_xml_error(); void destroy_xml_error(); /** * Used by gmpc to check against what version the plugin is compiled */ int plugin_api_version = PLUGIN_API_VERSION; /** * Get plugin priority, the lower the sooner checked */ static int fetch_priority() { return cfg_get_single_value_as_int_with_default(config, "lyric-provider", "priority", 90); } /** * Preferences structure */ gmpcPrefPlugin lyrics_gpp = { lyrics_construct, lyrics_destroy }; /** * Meta data structure */ gmpcMetaDataPlugin lyric_fetch = { fetch_priority, fetch_lyric }; gmpcPlugin plugin = { /** Name */ "Lyrics fetcher", /** Version */ {0,15,0}, /** Plugin type */ GMPC_PLUGIN_META_DATA, /** internal id */ 0, /** path where the plugin is located (filled in by gmpc) */ NULL, /** initialize function */ &lyrics_init, /* Destroy */ NULL, /** Browser plugin */ NULL, /** Status Changed signals */ NULL, /** Connection changed */ NULL, /** Preferences structure */ &lyrics_gpp, /** Meta data structure */ &lyric_fetch, /** get_enabled() */ lyrics_get_enabled, /** set enabled() */ lyrics_set_enabled }; static struct lyrics_api apis[] = { {"LyricWiki", "http://lyricwiki.org/server.php", TRUE, NULL, NULL, NULL, NULL, NULL, __lyricwiki_get_soap_message, __lyricwiki_get_soap_lyrics }, {"LeosLyrics", "http://api.leoslyrics.com/", FALSE, "api_search.php?auth=" PLUGIN_AUTH "&artist=%s&songtitle=%s", "api_search.php?auth=" PLUGIN_AUTH "&songtitle=%s", "api_lyrics.php?auth=" PLUGIN_AUTH "&hid=%s", __leoslyrics_get_id, __leoslyrics_get_lyrics, NULL, NULL }, {"Lyrics Tracker", "http://lyrictracker.com/soap.php?cln=" PLUGIN_AUTH "&clv=undef", FALSE, "&act=query&ar=%s&ti=%s", "&act=query&ti=%s", "&act=detail&id=%s", __lyrictracker_get_id, __lyrictracker_get_lyrics, NULL, NULL }, {NULL,NULL,FALSE,NULL,NULL,NULL, NULL, NULL, NULL, NULL} }; void xml_error_func(void * ctx, const char * msg, ...) { } // yes.. do this much on error void setup_xml_error() { if (handler==NULL) handler = (xmlGenericErrorFunc)xml_error_func; initGenericErrorDefaultFunc(&handler); } void destroy_xml_error() { initGenericErrorDefaultFunc((xmlGenericErrorFunc *)NULL); } static size_t post_write( void *ptr, size_t size, size_t nmemb, GString *d) { g_string_append_len(d, (gchar*)ptr, size*nmemb); return size*nmemb; } void init_post_message(post_message *s) { s->url = NULL; s->headers = NULL; s->body = NULL; s->response = NULL; s->response_code = -1; } /* the only thing not automagically freed is url */ void free_post_message(post_message *s) { if(s->body!=NULL) g_string_free(s->body, TRUE); if(s->response!=NULL) g_string_free(s->response, TRUE); if(s->headers) { curl_slist_free_all(s->headers); s->headers = NULL; } } void lyrics_init (void) { gchar *path = g_strdup_printf("%s/.lyrics/", g_get_home_dir()); if (!g_file_test(path, G_FILE_TEST_IS_DIR)) { g_mkdir(path, 0755); } g_free(path); } xmlNodePtr get_node_by_name (xmlNodePtr node, xmlChar *name) { xmlNodePtr cur; for (cur = node; cur; cur = cur->next) if (xmlStrEqual(cur->name, name) && (cur->type == XML_ELEMENT_NODE)) return cur; return NULL; } /* i think this interestingly named function urlencodes string */ static char * __lyrics_process_string(char *name) { int i = 0; int j = 0; int depth = 0; char *result = NULL; int length = strlen(name); for(i=0; i= 'a' && name[i] <= 'z') && !(name[i] >= 'A' && name[i] <= 'Z') && !(name[i] >= '0' && name[i] <= '9')) { length+=2; } } /** allocate */ /** TODO do this somewhere else */ result = g_malloc0((length+1)*sizeof(char)); depth = 0; for(i=0; i < strlen(name);i++) { if(name[i] == '(' || name[i] == '[' || name[i] == '{') { depth++; } else if(name[i] == ')' || name[i] == ']' || name[i] == '}') { depth--; } else if(depth >0) { /* do nothing */ } else if(!(name[i] >= 'a' && name[i] <= 'z') && !(name[i] >= 'A' && name[i] <= 'Z') && !(name[i] >= '0' && name[i] <= '9')) { snprintf(&result[j],4,"%%%02X", name[i]); j+=3; } else { result[j] = name[i]; j++; } } return result; } static int fetch_lyrics (mpd_Song *song, struct lyrics_api *api, gchar **lyrics, int exact) { gmpc_easy_download_struct dl = {NULL,0,-1, NULL, NULL}; xmlDocPtr results_doc; gchar *search_url, *lyrics_url, *temp; gchar *hid = NULL; gchar *data = NULL; gchar *esc_hid = NULL; // esc => escaped if (!lyrics) return 1; /* Check API */ if (!api->get_id || !api->get_lyrics) { return 1; } /* Fetch ID */ if (song->artist) { char *esc_artist = __lyrics_process_string(song->artist); char *esc_title = __lyrics_process_string(song->title); temp = g_strdup_printf("%s%s", api->host, api->search_full); search_url = g_strdup_printf(temp, esc_artist,esc_title); g_free(esc_artist); g_free(esc_title); g_free(temp); } else { char *esc_title = __lyrics_process_string(song->title); temp = g_strdup_printf("%s%s", api->host, api->search_title); search_url = g_strdup_printf(temp, esc_title); g_free(temp); g_free(esc_title); } debug_printf(DEBUG_INFO, "search url:'%s'\n", search_url); if (!gmpc_easy_download (search_url, &dl)) { g_free(search_url); return 1; } g_free(search_url); results_doc = xmlParseMemory(dl.data, dl.size); gmpc_easy_download_clean(&dl); if (!results_doc) { return 1; } /* Get the song ID from the results */ hid = api->get_id(results_doc, song->artist, song->title, exact); if (!hid || !strlen(hid)) { xmlFreeDoc(results_doc); xmlCleanupParser(); if (hid) xmlFree(hid); return 1; } xmlFreeDoc(results_doc); xmlCleanupParser(); esc_hid = __lyrics_process_string(hid); /* Fetch lyrics */ temp = g_strdup_printf("%s%s", api->host, api->lyrics_uri); lyrics_url = g_strdup_printf(temp, esc_hid); g_free(esc_hid); g_free(temp); if (!gmpc_easy_download (lyrics_url, &dl)) { xmlFree(hid); xmlCleanupParser(); g_free(lyrics_url); return 1; } g_free(lyrics_url); data = api->get_lyrics(&dl); /* Cleanup data */ gmpc_easy_download_clean(&dl); // wtf? /* i don't get it so i won't remove it completely.... * songtitle was fetched from get_songtitle * which as far as i can figure out is that if songtitle is equal to * song->title return no data found... now.. why would he do that... if(strcasecmp(songtitle, song->title)) { xmlFreeDoc(results_doc); xmlCleanupParser(); xmlFree(hid); xmlFree(data); xmlFree(songtitle); *lyrics = g_strdup(__STR_NODATA_ERROR); return 1; } */ if (!data || !strlen(data)) { xmlCleanupParser(); xmlFree(hid); xmlFree(data); return 1; } *lyrics = g_strdup(data); /* Cleanup and return */ xmlFree(data); xmlFree(hid); xmlCleanupParser(); return 0; } /* yeah, I know.. I'm great with names */ int do_post (post_message *msg) { int timeout; CURLcode c_ret; CURL *con; con = curl_easy_init(); if(!msg->url) { debug_printf(DEBUG_ERROR, "You really need a url in post_message\n"); return 0; } if(!msg->body) { debug_printf(DEBUG_ERROR, "You need a body in post_message\n"); return 0; } /* set timeout */ timeout = cfg_get_single_value_as_int_with_default(config, "Network Settings", "Connection Timeout", 10); curl_easy_setopt(con, CURLOPT_CONNECTTIMEOUT, timeout); curl_easy_setopt(con, CURLOPT_NOSIGNAL, TRUE); curl_easy_setopt(con, CURLOPT_URL, msg->url); if(cfg_get_single_value_as_int_with_default(config, "Network Settings", "Use Proxy", FALSE)) { char *value = cfg_get_single_value_as_string(config, "Network Settings", "Proxy Address"); int port = cfg_get_single_value_as_int_with_default(config, "Network Settings", "Proxy Port",8080); if(value) { curl_easy_setopt(con, CURLOPT_PROXY, value); curl_easy_setopt(con, CURLOPT_PROXYPORT, port); cfg_free_string(value); } else{ debug_printf(DEBUG_ERROR ,"Proxy enabled, but no proxy defined"); } } /* setting up callback function */ msg->response = g_string_sized_new(1024); // starting with 1KB curl_easy_setopt(con, CURLOPT_WRITEFUNCTION, post_write); curl_easy_setopt(con, CURLOPT_WRITEDATA, msg->response); /* we must use post */ curl_easy_setopt(con, CURLOPT_POST, TRUE); //curl_easy_setopt(con, CURLOPT_VERBOSE, 1); curl_easy_setopt(con, CURLOPT_POSTFIELDS, msg->body->str); curl_easy_setopt(con, CURLOPT_POSTFIELDSIZE, msg->body->len); // set headers if(msg->headers) curl_easy_setopt(con, CURLOPT_HTTPHEADER, msg->headers); /* perform request */ c_ret = curl_easy_perform(con); curl_easy_getinfo(con, CURLINFO_RESPONSE_CODE, &(msg->response_code)); //printf(msg->response->str); /* do curl cleanup now */ curl_slist_free_all(msg->headers); msg->headers = NULL; curl_easy_cleanup(con); if(c_ret) return 0; return 1; } void add_post_header(post_message *msg, gchar *header) { msg->headers = curl_slist_append(msg->headers, header); } static int fetch_lyrics_soap(mpd_Song *song, struct lyrics_api *api, gchar **lyrics, int exact) { int ret; xmlDocPtr xml; post_message request; /* check api */ if (!api->get_soap_message || !api->get_soap_lyrics) { return 1; } init_post_message(&request); ret = api->get_soap_message(&request, song->artist, song->title); if(!ret) { free_post_message(&request); return 1; } request.url = api->host; add_post_header(&request, USER_AGENT); add_post_header(&request, "Content-Type: text/xml; charset=UTF-8"); // TODO //slist = curl_slist_append(slist, "Expect:"); ret = do_post(&request); if(!ret) { free_post_message(&request); debug_printf(DEBUG_INFO, "got error from perform()\n"); return 1; } /* now, for the parsing */ xml = xmlParseMemory(request.response->str, request.response->len); free_post_message(&request); if(!xml) { xmlCleanupParser(); return 1; } *lyrics = api->get_soap_lyrics(xml, exact); xmlFreeDoc(xml); xmlCleanupParser(); if(*lyrics) return 0; return 1; } int fetch_lyric_loop(mpd_Song *song, gchar **lyrics, int preferred_api, int exact) { int ret = -1; int id = preferred_api; int last_id; setup_xml_error(); do { /* if we have something, free it -- it will be replaced */ if(*lyrics) g_free(*lyrics); debug_printf(DEBUG_INFO, "Search API: %s\n", apis[id].name); last_id = id; if(apis[id].soap) ret = fetch_lyrics_soap(song, &(apis[id]), lyrics, exact); else ret = fetch_lyrics(song, &(apis[id]), lyrics, exact); /* loop through api array */ if(id==preferred_api&&preferred_api!=0) { id=0; } else { if(++id==preferred_api&&apis[id].name != NULL) ++id; } } while(apis[id].name != NULL && !(!ret && *lyrics && strlen(*lyrics))); if(!ret && *lyrics && strlen(*lyrics)) { gchar *tmp = *lyrics; *lyrics = g_strjoin(NULL, *lyrics, LYRICS_FROM, apis[last_id].name, NULL); g_free(tmp); } //destroy_xml_error(); return ret; } int fetch_lyric(mpd_Song *song, MetaDataType type, char **path) { if(song == NULL || song->title== NULL || type != META_SONG_TXT) { return META_DATA_UNAVAILABLE; } if (song->title != NULL) { gchar *lyrics = NULL; int id, exact; id = cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "api-id", 0); exact = cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "exact-match", 1); int ret = 0; ret = fetch_lyric_loop(song, &lyrics, id, exact); if(!ret && lyrics && strlen(lyrics)) { *path = g_strdup_printf("%s/.lyrics/%s-%s.txt", g_get_home_dir(), song->artist, song->title); g_file_set_contents(*path, lyrics, -1, NULL); g_free(lyrics); return META_DATA_AVAILABLE; } if(lyrics) g_free(lyrics); } return META_DATA_UNAVAILABLE; } /** Called when enabling the plugin from the preferences dialog. Set * the configuration so that we'll know that the plugin is enabled * afterwards. */ void lyrics_enable_toggle (GtkWidget *wid) { int enable = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(wid)); cfg_set_single_value_as_int(config, "lyrics-plugin", "enable", enable); gtk_widget_set_sensitive(lyrics_pref_table, cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "enable", 0)); } static void lyrics_match_toggle (GtkWidget *wid) { int match = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(wid)); cfg_set_single_value_as_int(config, "lyrics-plugin", "exact-match", match); } /** Called when the user changes the lyrics API. Set a configuration * value to the new API id. */ void lyrics_api_changed (GtkWidget *wid) { int id = gtk_combo_box_get_active(GTK_COMBO_BOX(wid)); cfg_set_single_value_as_int(config, "lyrics-plugin", "api-id", id); debug_printf(DEBUG_INFO, "Saved API ID: %d\n", cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "api-id", 0)); } void lyrics_destroy (GtkWidget *container) { gtk_container_remove(GTK_CONTAINER(container), lyrics_pref_vbox); } /** * Initialize GTK widgets for the preferences window. */ void lyrics_construct (GtkWidget *container) { GtkWidget *enable_cg, *label, *combo, *match; int i; enable_cg = gtk_check_button_new_with_mnemonic("_Enable lyrics"); label = gtk_label_new("Preferred lyric site :"); combo = gtk_combo_box_new_text(); match = gtk_check_button_new_with_mnemonic("Exact _match only"); lyrics_pref_table = gtk_table_new(2, 2, FALSE); lyrics_pref_vbox = gtk_vbox_new(FALSE,6); for (i=0; apis[i].name ; i++) gtk_combo_box_append_text(GTK_COMBO_BOX(combo), apis[i].name); gtk_combo_box_set_active(GTK_COMBO_BOX(combo), cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "api-id", 0)); gtk_table_attach_defaults(GTK_TABLE(lyrics_pref_table), label, 0, 1, 0, 1); gtk_table_attach_defaults(GTK_TABLE(lyrics_pref_table), combo, 1, 2, 0, 1); gtk_table_attach_defaults(GTK_TABLE(lyrics_pref_table), match, 0, 2, 1, 2); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(enable_cg), cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "enable", 0)); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(match), cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "exact-match", 1)); gtk_widget_set_sensitive(lyrics_pref_table, cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "enable", 0)); /* TODO: check that this stuff actually works */ //gtk_widget_set_sensitive(match, 0); /* Connect signals */ g_signal_connect(G_OBJECT(combo), "changed", G_CALLBACK(lyrics_api_changed), NULL); g_signal_connect(G_OBJECT(enable_cg), "toggled", G_CALLBACK(lyrics_enable_toggle), NULL); g_signal_connect(G_OBJECT(match), "toggled", G_CALLBACK(lyrics_match_toggle), NULL); gtk_box_pack_start(GTK_BOX(lyrics_pref_vbox), enable_cg, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(lyrics_pref_vbox), lyrics_pref_table, FALSE, FALSE, 0); gtk_container_add(GTK_CONTAINER(container), lyrics_pref_vbox); gtk_widget_show_all(container); } int lyrics_get_enabled() { return cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "enable", 0); } void lyrics_set_enabled(int enabled) { cfg_set_single_value_as_int(config, "lyrics-plugin", "enable", enabled); }