gmpc-lyrics version 0.16.96
[gmpc-lyrics.git] / src / plugin.c
blob19ad13b488dd384088e68d1772b1fe149124398b
1 /* Copyright 2006 Maxime Petazzoni <maxime.petazzoni@bulix.org>
3 * gmpc-lyrics : A lyrics fetcher for GMpc
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2 of
8 * the License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21 #include "gmpc-lyrics.h"
22 #include <config.h>
24 #define LYRICS_FROM "\n\nLyric from "
26 static GtkWidget *lyrics_pref_vbox = NULL;
27 static GtkWidget *lyrics_pref_table = NULL;
29 static xmlGenericErrorFunc handler = NULL;
31 static gchar *escape_uri_string (const gchar *string);
32 static int shrink_string(gchar *string, int start, int end);
34 int lyrics_get_enabled();
35 void lyrics_set_enabled(int enabled);
36 /* Playlist window row reference */
37 int fetch_lyric(mpd_Song *song, MetaDataType type, char **path);
39 int fetch_lyric_loop(mpd_Song *song, gchar **lyrics, int preferred_api, int exact);
41 void xml_error_func(void * ctx, const char * msg, ...);
42 void setup_xml_error();
43 void destroy_xml_error();
45 /**
46 * Used by gmpc to check against what version the plugin is compiled
48 int plugin_api_version = PLUGIN_API_VERSION;
50 /**
51 * Get plugin priority, the lower the sooner checked
53 static int fetch_priority()
55 return cfg_get_single_value_as_int_with_default(config, "lyric-provider", "priority", 90);
57 static void set_priority(int priority)
59 cfg_set_single_value_as_int(config, "lyric-provider", "priority", priority);
62 /**
63 * Preferences structure
65 gmpcPrefPlugin lyrics_gpp = {
66 .construct = lyrics_construct,
67 .destroy = lyrics_destroy
70 /**
71 * Meta data structure
73 gmpcMetaDataPlugin lyric_fetch = {
74 .get_priority = fetch_priority,
75 .set_priority = set_priority,
76 .get_image = fetch_lyric
79 gmpcPlugin plugin = {
80 /** Name */
81 .name = "Lyrics fetcher",
82 /** Version */
83 .version = {PLUGIN_MAJOR_VERSION,PLUGIN_MINOR_VERSION,PLUGIN_MICRO_VERSION},
84 /** Plugin type */
85 .plugin_type = GMPC_PLUGIN_META_DATA,
86 /** initialize function */
87 .init = &lyrics_init,
88 /** Preferences structure */
89 .pref = &lyrics_gpp,
90 /** Meta data structure */
91 .metadata = &lyric_fetch,
92 /** get_enabled() */
93 .get_enabled = lyrics_get_enabled,
94 /** set enabled() */
95 .set_enabled = lyrics_set_enabled
98 static struct lyrics_api apis[] =
100 {"LyricWiki",
101 "http://lyricwiki.org/server.php",
102 TRUE,
103 NULL,
104 NULL,
105 NULL,
106 NULL,
107 NULL,
108 __lyricwiki_get_soap_message,
109 __lyricwiki_get_soap_lyrics
111 {"LeosLyrics",
112 "http://api.leoslyrics.com/",
113 FALSE,
114 "api_search.php?auth=" PLUGIN_AUTH "&artist=%s&songtitle=%s",
115 "api_search.php?auth=" PLUGIN_AUTH "&songtitle=%s",
116 "api_lyrics.php?auth=" PLUGIN_AUTH "&hid=%s",
117 __leoslyrics_get_id,
118 __leoslyrics_get_lyrics,
119 NULL,
120 NULL
122 {"Lyrics Tracker",
123 "http://lyrictracker.com/soap.php?cln=" PLUGIN_AUTH "&clv=undef",
124 FALSE,
125 "&act=query&ar=%s&ti=%s",
126 "&act=query&ti=%s",
127 "&act=detail&id=%s",
128 __lyrictracker_get_id,
129 __lyrictracker_get_lyrics,
130 NULL,
131 NULL
133 {NULL,NULL,FALSE,NULL,NULL,NULL, NULL, NULL, NULL, NULL}
137 void xml_error_func(void * ctx, const char * msg, ...) { } // yes.. do this much on error
139 void setup_xml_error() {
140 if (handler==NULL)
141 handler = (xmlGenericErrorFunc)xml_error_func;
142 initGenericErrorDefaultFunc(&handler);
144 void destroy_xml_error() {
145 initGenericErrorDefaultFunc((xmlGenericErrorFunc *)NULL);
148 static size_t post_write( void *ptr, size_t size, size_t nmemb, GString *d) {
149 g_string_append_len(d, (gchar*)ptr, size*nmemb);
150 return size*nmemb;
153 void init_post_message(post_message *s) {
154 s->url = NULL;
155 s->headers = NULL;
156 s->body = NULL;
157 s->response = NULL;
158 s->response_code = -1;
160 /* the only thing not automagically freed is url */
161 void free_post_message(post_message *s) {
162 if(s->body!=NULL)
163 g_string_free(s->body, TRUE);
164 if(s->response!=NULL)
165 g_string_free(s->response, TRUE);
166 if(s->headers) {
167 curl_slist_free_all(s->headers);
168 s->headers = NULL;
172 void lyrics_init (void)
174 /* gchar *path = g_build_path(G_DIR_SEPARATOR_S, g_get_home_dir(),".lyrics",NULL);
176 if (!g_file_test(path, G_FILE_TEST_IS_DIR))
178 g_mkdir(path, 0755);
181 g_free(path);
185 xmlNodePtr get_node_by_name (xmlNodePtr node, xmlChar *name)
187 xmlNodePtr cur;
189 for (cur = node; cur; cur = cur->next)
190 if (xmlStrEqual(cur->name, name) &&
191 (cur->type == XML_ELEMENT_NODE))
192 return cur;
194 return NULL;
197 static char * __lyrics_process_string(char *name)
199 return g_uri_escape_string(name, NULL, TRUE);//g_escape_uri_string(name);
202 static int fetch_lyrics (mpd_Song *song, struct lyrics_api *api, gchar **lyrics, int exact)
204 gmpc_easy_download_struct dl = {NULL,0,-1, NULL, NULL};
205 xmlDocPtr results_doc;
207 gchar *search_url, *lyrics_url, *temp;
208 gchar *hid = NULL;
209 gchar *data = NULL;
210 gchar *esc_hid = NULL; // esc => escaped
212 if (!lyrics)
213 return 1;
215 /* Check API */
216 if (!api->get_id || !api->get_lyrics)
218 return 1;
221 /* Fetch ID */
222 if (song->artist)
224 char *esc_artist = __lyrics_process_string(song->artist);
225 char *esc_title = __lyrics_process_string(song->title);
226 temp = g_strdup_printf("%s%s", api->host, api->search_full);
227 search_url = g_strdup_printf(temp, esc_artist,esc_title);
228 g_free(esc_artist);
229 g_free(esc_title);
230 g_free(temp);
232 else
234 char *esc_title = __lyrics_process_string(song->title);
235 temp = g_strdup_printf("%s%s", api->host, api->search_title);
236 search_url = g_strdup_printf(temp, esc_title);
237 g_free(temp);
238 g_free(esc_title);
240 debug_printf(DEBUG_INFO, "search url:'%s'\n", search_url);
242 if (!gmpc_easy_download (search_url, &dl))
244 g_free(search_url);
245 return 1;
247 g_free(search_url);
248 results_doc = xmlParseMemory(dl.data, dl.size);
249 gmpc_easy_download_clean(&dl);
250 if (!results_doc)
252 return 1;
255 /* Get the song ID from the results */
256 hid = api->get_id(results_doc, song->artist, song->title, exact);
257 if (!hid || !strlen(hid))
259 xmlFreeDoc(results_doc);
262 if (hid)
263 xmlFree(hid);
265 return 1;
267 xmlFreeDoc(results_doc);
270 esc_hid = __lyrics_process_string(hid);
272 /* Fetch lyrics */
273 temp = g_strdup_printf("%s%s", api->host, api->lyrics_uri);
274 lyrics_url = g_strdup_printf(temp, esc_hid);
275 g_free(esc_hid);
276 g_free(temp);
278 if (!gmpc_easy_download (lyrics_url, &dl))
280 xmlFree(hid);
283 g_free(lyrics_url);
284 return 1;
286 g_free(lyrics_url);
288 data = api->get_lyrics(&dl);
289 /* Cleanup data */
290 gmpc_easy_download_clean(&dl);
292 // wtf?
293 /* i don't get it so i won't remove it completely....
294 * songtitle was fetched from get_songtitle
295 * which as far as i can figure out is that if songtitle is equal to
296 * song->title return no data found... now.. why would he do that...
297 if(strcasecmp(songtitle, song->title))
299 xmlFreeDoc(results_doc);
302 xmlFree(hid);
303 xmlFree(data);
304 xmlFree(songtitle);
306 *lyrics = g_strdup(__STR_NODATA_ERROR);
307 return 1;
310 if (!data || !strlen(data))
313 xmlFree(hid);
314 xmlFree(data);
316 return 1;
320 *lyrics = g_strdup(data);
321 /* Cleanup and return */
322 xmlFree(data);
323 xmlFree(hid);
326 return 0;
329 /* yeah, I know.. I'm great with names */
330 int do_post (post_message *msg) {
331 int timeout;
332 CURLcode c_ret;
333 CURL *con;
334 con = curl_easy_init();
336 if(!msg->url) {
337 debug_printf(DEBUG_ERROR, "You really need a url in post_message\n");
338 return 0;
340 if(!msg->body) {
341 debug_printf(DEBUG_ERROR, "You need a body in post_message\n");
342 return 0;
344 /* set timeout */
345 timeout = cfg_get_single_value_as_int_with_default(config, "Network Settings", "Connection Timeout", 10);
346 curl_easy_setopt(con, CURLOPT_CONNECTTIMEOUT, timeout);
347 curl_easy_setopt(con, CURLOPT_NOSIGNAL, TRUE);
349 curl_easy_setopt(con, CURLOPT_URL, msg->url);
351 if(cfg_get_single_value_as_int_with_default(config, "Network Settings", "Use Proxy", FALSE))
353 char *value = cfg_get_single_value_as_string(config, "Network Settings", "Proxy Address");
354 int port = cfg_get_single_value_as_int_with_default(config, "Network Settings", "Proxy Port",8080);
355 if(value)
357 curl_easy_setopt(con, CURLOPT_PROXY, value);
358 curl_easy_setopt(con, CURLOPT_PROXYPORT, port);
359 cfg_free_string(value);
361 else{
362 debug_printf(DEBUG_ERROR ,"Proxy enabled, but no proxy defined");
366 /* setting up callback function */
367 msg->response = g_string_sized_new(1024); // starting with 1KB
368 curl_easy_setopt(con, CURLOPT_WRITEFUNCTION, post_write);
369 curl_easy_setopt(con, CURLOPT_WRITEDATA, msg->response);
371 /* we must use post */
372 curl_easy_setopt(con, CURLOPT_POST, TRUE);
374 //curl_easy_setopt(con, CURLOPT_VERBOSE, 1);
375 curl_easy_setopt(con, CURLOPT_POSTFIELDS, msg->body->str);
376 curl_easy_setopt(con, CURLOPT_POSTFIELDSIZE, msg->body->len);
378 // set headers
379 if(msg->headers)
380 curl_easy_setopt(con, CURLOPT_HTTPHEADER, msg->headers);
382 /* perform request */
383 c_ret = curl_easy_perform(con);
385 curl_easy_getinfo(con, CURLINFO_RESPONSE_CODE, &(msg->response_code));
387 /* do curl cleanup now */
388 curl_slist_free_all(msg->headers);
389 msg->headers = NULL;
390 curl_easy_cleanup(con);
392 if(c_ret)
393 return 0;
394 return 1;
397 void add_post_header(post_message *msg, gchar *header) {
398 msg->headers = curl_slist_append(msg->headers, header);
401 static int fetch_lyrics_soap(mpd_Song *song, struct lyrics_api *api, gchar **lyrics, int exact) {
402 int ret;
404 xmlDocPtr xml;
405 post_message request;
407 /* check api */
408 if (!api->get_soap_message || !api->get_soap_lyrics) {
409 return 1;
412 init_post_message(&request);
414 /* don't escape artist/title here, it isn't needed */
415 ret = api->get_soap_message(&request, song->artist, song->title);
417 if(!ret) {
418 free_post_message(&request);
419 return 1;
421 request.url = api->host;
423 add_post_header(&request, USER_AGENT);
424 add_post_header(&request, "Content-Type: text/xml; charset=UTF-8"); // TODO
425 //slist = curl_slist_append(slist, "Expect:");
426 ret = do_post(&request);
428 if(!ret) {
429 free_post_message(&request);
430 debug_printf(DEBUG_INFO, "got error from perform()\n");
431 return 1;
434 /* now, for the parsing */
435 xml = xmlParseMemory(request.response->str, request.response->len);
436 free_post_message(&request);
438 if(!xml) {
440 return 1;
443 *lyrics = api->get_soap_lyrics(xml, exact);
445 xmlFreeDoc(xml);
448 if(*lyrics)
449 return 0;
450 return 1;
453 int fetch_lyric_loop(mpd_Song *song, gchar **lyrics, int preferred_api, int exact) {
454 int ret = -1;
455 int id = preferred_api;
456 int last_id;
459 setup_xml_error();
461 do {
462 /* if we have something, free it -- it will be replaced */
463 if(*lyrics)
464 g_free(*lyrics);
465 debug_printf(DEBUG_INFO, "Search API: %s\n", apis[id].name);
466 last_id = id;
468 if(apis[id].soap)
469 ret = fetch_lyrics_soap(song, &(apis[id]), lyrics, exact);
470 else
471 ret = fetch_lyrics(song, &(apis[id]), lyrics, exact);
473 /* loop through api array */
474 if(id==preferred_api&&preferred_api!=0) {
475 id=0;
477 else {
478 if(++id==preferred_api&&apis[id].name != NULL)
479 ++id;
481 } while(apis[id].name != NULL && !(!ret && *lyrics && strlen(*lyrics)));
483 if(!ret && *lyrics && strlen(*lyrics)) {
484 gchar *tmp = *lyrics;
485 *lyrics = g_strjoin(NULL, *lyrics, LYRICS_FROM, apis[last_id].name, NULL);
486 g_free(tmp);
488 //destroy_xml_error();
490 return ret;
493 int fetch_lyric(mpd_Song *song, MetaDataType type, char **path)
495 if(song == NULL || song->title== NULL || type != META_SONG_TXT)
497 return META_DATA_UNAVAILABLE;
499 if (song->title != NULL)
501 gchar *lyrics = NULL;
502 int id, exact;
504 id = cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "api-id", 0);
505 exact = cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "exact-match", 1);
507 int ret = 0;
508 ret = fetch_lyric_loop(song, &lyrics, id, exact);
509 if(!ret && lyrics && strlen(lyrics))
511 gchar *filename = g_strdup_printf("%s-%s.lyric", song->artist, song->title);
512 *path = gmpc_get_metadata_filename(META_SONG_TXT,song,NULL); //gmpc_get_covers_path(filename);
513 g_file_set_contents(*path, lyrics, -1, NULL);
514 g_free(lyrics);
515 return META_DATA_AVAILABLE;
517 if(lyrics)
518 g_free(lyrics);
520 return META_DATA_UNAVAILABLE;
524 /** Called when enabling the plugin from the preferences dialog. Set
525 * the configuration so that we'll know that the plugin is enabled
526 * afterwards.
528 void lyrics_enable_toggle (GtkWidget *wid)
530 int enable = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(wid));
531 cfg_set_single_value_as_int(config, "lyrics-plugin", "enable", enable);
532 gtk_widget_set_sensitive(lyrics_pref_table, cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "enable", 0));
535 static void lyrics_match_toggle (GtkWidget *wid)
537 int match = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(wid));
538 cfg_set_single_value_as_int(config, "lyrics-plugin", "exact-match", match);
542 /** Called when the user changes the lyrics API. Set a configuration
543 * value to the new API id.
545 void lyrics_api_changed (GtkWidget *wid)
547 int id = gtk_combo_box_get_active(GTK_COMBO_BOX(wid));
548 cfg_set_single_value_as_int(config, "lyrics-plugin", "api-id", id);
550 debug_printf(DEBUG_INFO, "Saved API ID: %d\n", cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "api-id", 0));
553 void lyrics_destroy (GtkWidget *container)
555 gtk_container_remove(GTK_CONTAINER(container), lyrics_pref_vbox);
558 /**
559 * Initialize GTK widgets for the preferences window.
561 void lyrics_construct (GtkWidget *container)
563 GtkWidget *enable_cg, *label, *combo, *match;
564 int i;
566 enable_cg = gtk_check_button_new_with_mnemonic("_Enable lyrics");
567 label = gtk_label_new("Preferred lyric site :");
568 combo = gtk_combo_box_new_text();
569 match = gtk_check_button_new_with_mnemonic("Exact _match only");
571 lyrics_pref_table = gtk_table_new(2, 2, FALSE);
572 lyrics_pref_vbox = gtk_vbox_new(FALSE,6);
574 for (i=0; apis[i].name ; i++)
575 gtk_combo_box_append_text(GTK_COMBO_BOX(combo), apis[i].name);
577 gtk_combo_box_set_active(GTK_COMBO_BOX(combo),
578 cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "api-id", 0));
580 gtk_table_attach_defaults(GTK_TABLE(lyrics_pref_table), label, 0, 1, 0, 1);
581 gtk_table_attach_defaults(GTK_TABLE(lyrics_pref_table), combo, 1, 2, 0, 1);
582 gtk_table_attach_defaults(GTK_TABLE(lyrics_pref_table), match, 0, 2, 1, 2);
584 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(enable_cg),
585 cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "enable", 0));
586 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(match),
587 cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "exact-match", 1));
589 gtk_widget_set_sensitive(lyrics_pref_table, cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "enable", 0));
591 /* TODO: check that this stuff actually works */
592 //gtk_widget_set_sensitive(match, 0);
594 /* Connect signals */
595 g_signal_connect(G_OBJECT(combo), "changed", G_CALLBACK(lyrics_api_changed), NULL);
596 g_signal_connect(G_OBJECT(enable_cg), "toggled", G_CALLBACK(lyrics_enable_toggle), NULL);
597 g_signal_connect(G_OBJECT(match), "toggled", G_CALLBACK(lyrics_match_toggle), NULL);
599 gtk_box_pack_start(GTK_BOX(lyrics_pref_vbox), enable_cg, FALSE, FALSE, 0);
600 gtk_box_pack_start(GTK_BOX(lyrics_pref_vbox), lyrics_pref_table, FALSE, FALSE, 0);
602 gtk_container_add(GTK_CONTAINER(container), lyrics_pref_vbox);
603 gtk_widget_show_all(container);
606 int lyrics_get_enabled()
608 return cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "enable", 0);
611 void lyrics_set_enabled(int enabled)
613 cfg_set_single_value_as_int(config, "lyrics-plugin", "enable", enabled);
616 static gchar *escape_uri_string (const gchar *string)
618 #define ACCEPTABLE(a) (((a) >= 'a' && (a) <= 'z') || ((a) >= 'A' && (a) <= 'Z') || ((a) >= '0' && (a) <= '9'))
620 const gchar hex[16] = "0123456789ABCDEF";
621 const gchar *p;
622 gchar *q;
623 gchar *result;
624 int c;
625 gint unacceptable = 0;
626 const gchar *tmp_p;
627 gchar *new_string;
628 int depth = 0;
629 int len;
630 int i = 0;
632 len = strlen(string);
634 new_string = g_malloc(len + 1);
636 /* Get count of chars that will need to be converted to %
637 and remove ([{}]) and everything between */
638 for (p = string; *p != '\0'; p++) {
639 c = (guchar) *p;
641 if(c == '(' || c == '[' || c == '{') {
642 depth++;
643 } else if(c == ')' || c == ']' || c == '}') {
644 depth--;
645 if(depth < 0)
646 depth = 0;
647 } else if(depth == 0) {
648 if (!ACCEPTABLE (c))
649 unacceptable++;
651 new_string[i] = c;
653 i++;
657 new_string[i] = '\0';
659 len = strlen(new_string);
661 /* remove double spaces from the string because removing ([{}])
662 tends to create those */
663 for(p = new_string + 1; *p != '\0'; p++) {
664 c = (guchar) *p;
665 if(c == ' ') {
666 tmp_p = p - 1;
667 if( *tmp_p == ' ') {
668 len = shrink_string(new_string, p - new_string, len);
669 p--;
674 /* make sure first char isn't a space */
675 if(new_string[0] == ' ')
676 len = shrink_string(new_string, 0, len);
678 /* make sure there isn't a trailing space*/
679 if(new_string[len - 1] == ' ')
680 len--;
682 new_string[len] = '\0';
684 result = g_malloc (len + unacceptable * 2 + 1);
686 /*time to create the escaped string*/
687 for (q = result, p = new_string; *p != '\0'; p++)
689 c = (guchar) *p;
691 if (!ACCEPTABLE (c)) {
692 *q++ = '%'; /* means hex coming */
693 *q++ = hex[c >> 4];
694 *q++ = hex[c & 15];
696 else
697 *q++ = *p;
700 *q = '\0';
702 g_free(new_string);
704 return result;
707 static int shrink_string(gchar *string, int start, int end)
709 int i;
711 for( i = start; i < end; i++)
712 string[i] = string[i + 1];
714 end--;
716 return end;