Fix versioning
[gmpc-lyrics.git] / src / plugin.c
blobc18a8a8870efbf75c564cc659b2e6e3ae25fcfe9
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);
58 /**
59 * Preferences structure
61 gmpcPrefPlugin lyrics_gpp = {
62 .construct = lyrics_construct,
63 .destroy = lyrics_destroy
66 /**
67 * Meta data structure
69 gmpcMetaDataPlugin lyric_fetch = {
70 .get_priority = fetch_priority,
71 .get_image = fetch_lyric
74 gmpcPlugin plugin = {
75 /** Name */
76 .name = "Lyrics fetcher",
77 /** Version */
78 .version = {PLUGIN_MAJOR_VERSION,PLUGIN_MINOR_VERSION,PLUGIN_MICRO_VERSION},
79 /** Plugin type */
80 .plugin_type = GMPC_PLUGIN_META_DATA,
81 /** initialize function */
82 .init = &lyrics_init,
83 /** Preferences structure */
84 .pref = &lyrics_gpp,
85 /** Meta data structure */
86 .metadata = &lyric_fetch,
87 /** get_enabled() */
88 .get_enabled = lyrics_get_enabled,
89 /** set enabled() */
90 .set_enabled = lyrics_set_enabled
93 static struct lyrics_api apis[] =
95 {"LyricWiki",
96 "http://lyricwiki.org/server.php",
97 TRUE,
98 NULL,
99 NULL,
100 NULL,
101 NULL,
102 NULL,
103 __lyricwiki_get_soap_message,
104 __lyricwiki_get_soap_lyrics
106 {"LeosLyrics",
107 "http://api.leoslyrics.com/",
108 FALSE,
109 "api_search.php?auth=" PLUGIN_AUTH "&artist=%s&songtitle=%s",
110 "api_search.php?auth=" PLUGIN_AUTH "&songtitle=%s",
111 "api_lyrics.php?auth=" PLUGIN_AUTH "&hid=%s",
112 __leoslyrics_get_id,
113 __leoslyrics_get_lyrics,
114 NULL,
115 NULL
117 {"Lyrics Tracker",
118 "http://lyrictracker.com/soap.php?cln=" PLUGIN_AUTH "&clv=undef",
119 FALSE,
120 "&act=query&ar=%s&ti=%s",
121 "&act=query&ti=%s",
122 "&act=detail&id=%s",
123 __lyrictracker_get_id,
124 __lyrictracker_get_lyrics,
125 NULL,
126 NULL
128 {NULL,NULL,FALSE,NULL,NULL,NULL, NULL, NULL, NULL, NULL}
132 void xml_error_func(void * ctx, const char * msg, ...) { } // yes.. do this much on error
134 void setup_xml_error() {
135 if (handler==NULL)
136 handler = (xmlGenericErrorFunc)xml_error_func;
137 initGenericErrorDefaultFunc(&handler);
139 void destroy_xml_error() {
140 initGenericErrorDefaultFunc((xmlGenericErrorFunc *)NULL);
143 static size_t post_write( void *ptr, size_t size, size_t nmemb, GString *d) {
144 g_string_append_len(d, (gchar*)ptr, size*nmemb);
145 return size*nmemb;
148 void init_post_message(post_message *s) {
149 s->url = NULL;
150 s->headers = NULL;
151 s->body = NULL;
152 s->response = NULL;
153 s->response_code = -1;
155 /* the only thing not automagically freed is url */
156 void free_post_message(post_message *s) {
157 if(s->body!=NULL)
158 g_string_free(s->body, TRUE);
159 if(s->response!=NULL)
160 g_string_free(s->response, TRUE);
161 if(s->headers) {
162 curl_slist_free_all(s->headers);
163 s->headers = NULL;
167 void lyrics_init (void)
169 /* gchar *path = g_build_path(G_DIR_SEPARATOR_S, g_get_home_dir(),".lyrics",NULL);
171 if (!g_file_test(path, G_FILE_TEST_IS_DIR))
173 g_mkdir(path, 0755);
176 g_free(path);
180 xmlNodePtr get_node_by_name (xmlNodePtr node, xmlChar *name)
182 xmlNodePtr cur;
184 for (cur = node; cur; cur = cur->next)
185 if (xmlStrEqual(cur->name, name) &&
186 (cur->type == XML_ELEMENT_NODE))
187 return cur;
189 return NULL;
192 static char * __lyrics_process_string(char *name)
194 return escape_uri_string(name);
197 static int fetch_lyrics (mpd_Song *song, struct lyrics_api *api, gchar **lyrics, int exact)
199 gmpc_easy_download_struct dl = {NULL,0,-1, NULL, NULL};
200 xmlDocPtr results_doc;
202 gchar *search_url, *lyrics_url, *temp;
203 gchar *hid = NULL;
204 gchar *data = NULL;
205 gchar *esc_hid = NULL; // esc => escaped
207 if (!lyrics)
208 return 1;
210 /* Check API */
211 if (!api->get_id || !api->get_lyrics)
213 return 1;
216 /* Fetch ID */
217 if (song->artist)
219 char *esc_artist = __lyrics_process_string(song->artist);
220 char *esc_title = __lyrics_process_string(song->title);
221 temp = g_strdup_printf("%s%s", api->host, api->search_full);
222 search_url = g_strdup_printf(temp, esc_artist,esc_title);
223 g_free(esc_artist);
224 g_free(esc_title);
225 g_free(temp);
227 else
229 char *esc_title = __lyrics_process_string(song->title);
230 temp = g_strdup_printf("%s%s", api->host, api->search_title);
231 search_url = g_strdup_printf(temp, esc_title);
232 g_free(temp);
233 g_free(esc_title);
235 debug_printf(DEBUG_INFO, "search url:'%s'\n", search_url);
237 if (!gmpc_easy_download (search_url, &dl))
239 g_free(search_url);
240 return 1;
242 g_free(search_url);
243 results_doc = xmlParseMemory(dl.data, dl.size);
244 gmpc_easy_download_clean(&dl);
245 if (!results_doc)
247 return 1;
250 /* Get the song ID from the results */
251 hid = api->get_id(results_doc, song->artist, song->title, exact);
252 if (!hid || !strlen(hid))
254 xmlFreeDoc(results_doc);
255 xmlCleanupParser();
257 if (hid)
258 xmlFree(hid);
260 return 1;
262 xmlFreeDoc(results_doc);
263 xmlCleanupParser();
265 esc_hid = __lyrics_process_string(hid);
267 /* Fetch lyrics */
268 temp = g_strdup_printf("%s%s", api->host, api->lyrics_uri);
269 lyrics_url = g_strdup_printf(temp, esc_hid);
270 g_free(esc_hid);
271 g_free(temp);
273 if (!gmpc_easy_download (lyrics_url, &dl))
275 xmlFree(hid);
276 xmlCleanupParser();
278 g_free(lyrics_url);
279 return 1;
281 g_free(lyrics_url);
283 data = api->get_lyrics(&dl);
284 /* Cleanup data */
285 gmpc_easy_download_clean(&dl);
287 // wtf?
288 /* i don't get it so i won't remove it completely....
289 * songtitle was fetched from get_songtitle
290 * which as far as i can figure out is that if songtitle is equal to
291 * song->title return no data found... now.. why would he do that...
292 if(strcasecmp(songtitle, song->title))
294 xmlFreeDoc(results_doc);
295 xmlCleanupParser();
297 xmlFree(hid);
298 xmlFree(data);
299 xmlFree(songtitle);
301 *lyrics = g_strdup(__STR_NODATA_ERROR);
302 return 1;
305 if (!data || !strlen(data))
307 xmlCleanupParser();
308 xmlFree(hid);
309 xmlFree(data);
311 return 1;
315 *lyrics = g_strdup(data);
316 /* Cleanup and return */
317 xmlFree(data);
318 xmlFree(hid);
319 xmlCleanupParser();
321 return 0;
324 /* yeah, I know.. I'm great with names */
325 int do_post (post_message *msg) {
326 int timeout;
327 CURLcode c_ret;
328 CURL *con;
329 con = curl_easy_init();
331 if(!msg->url) {
332 debug_printf(DEBUG_ERROR, "You really need a url in post_message\n");
333 return 0;
335 if(!msg->body) {
336 debug_printf(DEBUG_ERROR, "You need a body in post_message\n");
337 return 0;
339 /* set timeout */
340 timeout = cfg_get_single_value_as_int_with_default(config, "Network Settings", "Connection Timeout", 10);
341 curl_easy_setopt(con, CURLOPT_CONNECTTIMEOUT, timeout);
342 curl_easy_setopt(con, CURLOPT_NOSIGNAL, TRUE);
344 curl_easy_setopt(con, CURLOPT_URL, msg->url);
346 if(cfg_get_single_value_as_int_with_default(config, "Network Settings", "Use Proxy", FALSE))
348 char *value = cfg_get_single_value_as_string(config, "Network Settings", "Proxy Address");
349 int port = cfg_get_single_value_as_int_with_default(config, "Network Settings", "Proxy Port",8080);
350 if(value)
352 curl_easy_setopt(con, CURLOPT_PROXY, value);
353 curl_easy_setopt(con, CURLOPT_PROXYPORT, port);
354 cfg_free_string(value);
356 else{
357 debug_printf(DEBUG_ERROR ,"Proxy enabled, but no proxy defined");
361 /* setting up callback function */
362 msg->response = g_string_sized_new(1024); // starting with 1KB
363 curl_easy_setopt(con, CURLOPT_WRITEFUNCTION, post_write);
364 curl_easy_setopt(con, CURLOPT_WRITEDATA, msg->response);
366 /* we must use post */
367 curl_easy_setopt(con, CURLOPT_POST, TRUE);
369 //curl_easy_setopt(con, CURLOPT_VERBOSE, 1);
370 curl_easy_setopt(con, CURLOPT_POSTFIELDS, msg->body->str);
371 curl_easy_setopt(con, CURLOPT_POSTFIELDSIZE, msg->body->len);
373 // set headers
374 if(msg->headers)
375 curl_easy_setopt(con, CURLOPT_HTTPHEADER, msg->headers);
377 /* perform request */
378 c_ret = curl_easy_perform(con);
380 curl_easy_getinfo(con, CURLINFO_RESPONSE_CODE, &(msg->response_code));
382 /* do curl cleanup now */
383 curl_slist_free_all(msg->headers);
384 msg->headers = NULL;
385 curl_easy_cleanup(con);
387 if(c_ret)
388 return 0;
389 return 1;
392 void add_post_header(post_message *msg, gchar *header) {
393 msg->headers = curl_slist_append(msg->headers, header);
396 static int fetch_lyrics_soap(mpd_Song *song, struct lyrics_api *api, gchar **lyrics, int exact) {
397 int ret;
399 xmlDocPtr xml;
400 post_message request;
402 /* check api */
403 if (!api->get_soap_message || !api->get_soap_lyrics) {
404 return 1;
407 init_post_message(&request);
408 char *esc_artist = __lyrics_process_string(song->artist);
409 char *esc_title = __lyrics_process_string(song->title);
410 ret = api->get_soap_message(&request, esc_artist, esc_title);
411 g_free(esc_artist);
412 g_free(esc_title);
414 if(!ret) {
415 free_post_message(&request);
416 return 1;
418 request.url = api->host;
420 add_post_header(&request, USER_AGENT);
421 add_post_header(&request, "Content-Type: text/xml; charset=UTF-8"); // TODO
422 //slist = curl_slist_append(slist, "Expect:");
423 ret = do_post(&request);
425 if(!ret) {
426 free_post_message(&request);
427 debug_printf(DEBUG_INFO, "got error from perform()\n");
428 return 1;
431 /* now, for the parsing */
432 xml = xmlParseMemory(request.response->str, request.response->len);
433 free_post_message(&request);
435 if(!xml) {
436 xmlCleanupParser();
437 return 1;
440 *lyrics = api->get_soap_lyrics(xml, exact);
442 xmlFreeDoc(xml);
443 xmlCleanupParser();
445 if(*lyrics)
446 return 0;
447 return 1;
450 int fetch_lyric_loop(mpd_Song *song, gchar **lyrics, int preferred_api, int exact) {
451 int ret = -1;
452 int id = preferred_api;
453 int last_id;
456 setup_xml_error();
458 do {
459 /* if we have something, free it -- it will be replaced */
460 if(*lyrics)
461 g_free(*lyrics);
462 debug_printf(DEBUG_INFO, "Search API: %s\n", apis[id].name);
463 last_id = id;
465 if(apis[id].soap)
466 ret = fetch_lyrics_soap(song, &(apis[id]), lyrics, exact);
467 else
468 ret = fetch_lyrics(song, &(apis[id]), lyrics, exact);
470 /* loop through api array */
471 if(id==preferred_api&&preferred_api!=0) {
472 id=0;
474 else {
475 if(++id==preferred_api&&apis[id].name != NULL)
476 ++id;
478 } while(apis[id].name != NULL && !(!ret && *lyrics && strlen(*lyrics)));
480 if(!ret && *lyrics && strlen(*lyrics)) {
481 gchar *tmp = *lyrics;
482 *lyrics = g_strjoin(NULL, *lyrics, LYRICS_FROM, apis[last_id].name, NULL);
483 g_free(tmp);
485 //destroy_xml_error();
487 return ret;
490 int fetch_lyric(mpd_Song *song, MetaDataType type, char **path)
492 if(song == NULL || song->title== NULL || type != META_SONG_TXT)
494 return META_DATA_UNAVAILABLE;
496 if (song->title != NULL)
498 gchar *lyrics = NULL;
499 int id, exact;
501 id = cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "api-id", 0);
502 exact = cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "exact-match", 1);
504 int ret = 0;
505 ret = fetch_lyric_loop(song, &lyrics, id, exact);
506 if(!ret && lyrics && strlen(lyrics))
508 gchar *filename = g_strdup_printf("%s-%s.lyric", song->artist, song->title);
509 *path = gmpc_get_covers_path(filename);
510 g_file_set_contents(*path, lyrics, -1, NULL);
511 g_free(lyrics);
512 return META_DATA_AVAILABLE;
514 if(lyrics)
515 g_free(lyrics);
517 return META_DATA_UNAVAILABLE;
521 /** Called when enabling the plugin from the preferences dialog. Set
522 * the configuration so that we'll know that the plugin is enabled
523 * afterwards.
525 void lyrics_enable_toggle (GtkWidget *wid)
527 int enable = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(wid));
528 cfg_set_single_value_as_int(config, "lyrics-plugin", "enable", enable);
529 gtk_widget_set_sensitive(lyrics_pref_table, cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "enable", 0));
532 static void lyrics_match_toggle (GtkWidget *wid)
534 int match = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(wid));
535 cfg_set_single_value_as_int(config, "lyrics-plugin", "exact-match", match);
539 /** Called when the user changes the lyrics API. Set a configuration
540 * value to the new API id.
542 void lyrics_api_changed (GtkWidget *wid)
544 int id = gtk_combo_box_get_active(GTK_COMBO_BOX(wid));
545 cfg_set_single_value_as_int(config, "lyrics-plugin", "api-id", id);
547 debug_printf(DEBUG_INFO, "Saved API ID: %d\n", cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "api-id", 0));
550 void lyrics_destroy (GtkWidget *container)
552 gtk_container_remove(GTK_CONTAINER(container), lyrics_pref_vbox);
555 /**
556 * Initialize GTK widgets for the preferences window.
558 void lyrics_construct (GtkWidget *container)
560 GtkWidget *enable_cg, *label, *combo, *match;
561 int i;
563 enable_cg = gtk_check_button_new_with_mnemonic("_Enable lyrics");
564 label = gtk_label_new("Preferred lyric site :");
565 combo = gtk_combo_box_new_text();
566 match = gtk_check_button_new_with_mnemonic("Exact _match only");
568 lyrics_pref_table = gtk_table_new(2, 2, FALSE);
569 lyrics_pref_vbox = gtk_vbox_new(FALSE,6);
571 for (i=0; apis[i].name ; i++)
572 gtk_combo_box_append_text(GTK_COMBO_BOX(combo), apis[i].name);
574 gtk_combo_box_set_active(GTK_COMBO_BOX(combo),
575 cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "api-id", 0));
577 gtk_table_attach_defaults(GTK_TABLE(lyrics_pref_table), label, 0, 1, 0, 1);
578 gtk_table_attach_defaults(GTK_TABLE(lyrics_pref_table), combo, 1, 2, 0, 1);
579 gtk_table_attach_defaults(GTK_TABLE(lyrics_pref_table), match, 0, 2, 1, 2);
581 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(enable_cg),
582 cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "enable", 0));
583 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(match),
584 cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "exact-match", 1));
586 gtk_widget_set_sensitive(lyrics_pref_table, cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "enable", 0));
588 /* TODO: check that this stuff actually works */
589 //gtk_widget_set_sensitive(match, 0);
591 /* Connect signals */
592 g_signal_connect(G_OBJECT(combo), "changed", G_CALLBACK(lyrics_api_changed), NULL);
593 g_signal_connect(G_OBJECT(enable_cg), "toggled", G_CALLBACK(lyrics_enable_toggle), NULL);
594 g_signal_connect(G_OBJECT(match), "toggled", G_CALLBACK(lyrics_match_toggle), NULL);
596 gtk_box_pack_start(GTK_BOX(lyrics_pref_vbox), enable_cg, FALSE, FALSE, 0);
597 gtk_box_pack_start(GTK_BOX(lyrics_pref_vbox), lyrics_pref_table, FALSE, FALSE, 0);
599 gtk_container_add(GTK_CONTAINER(container), lyrics_pref_vbox);
600 gtk_widget_show_all(container);
603 int lyrics_get_enabled()
605 return cfg_get_single_value_as_int_with_default(config, "lyrics-plugin", "enable", 0);
608 void lyrics_set_enabled(int enabled)
610 cfg_set_single_value_as_int(config, "lyrics-plugin", "enable", enabled);
613 static gchar *escape_uri_string (const gchar *string)
615 #define ACCEPTABLE(a) (((a) >= 'a' && (a) <= 'z') || ((a) >= 'A' && (a) <= 'Z') || ((a) >= '0' && (a) <= '9'))
617 const gchar hex[16] = "0123456789ABCDEF";
618 const gchar *p;
619 gchar *q;
620 gchar *result;
621 int c;
622 gint unacceptable = 0;
623 const gchar *tmp_p;
624 gchar *new_string;
625 int depth = 0;
626 int len;
627 int i = 0;
629 len = strlen(string);
631 new_string = g_malloc(len + 1);
633 /* Get count of chars that will need to be converted to %
634 and remove ([{}]) and everything between */
635 for (p = string; *p != '\0'; p++) {
636 c = (guchar) *p;
638 if(c == '(' || c == '[' || c == '{') {
639 depth++;
640 } else if(c == ')' || c == ']' || c == '}') {
641 depth--;
642 if(depth < 0)
643 depth = 0;
644 } else if(depth == 0) {
645 if (!ACCEPTABLE (c))
646 unacceptable++;
648 new_string[i] = c;
650 i++;
654 new_string[i] = '\0';
656 len = strlen(new_string);
658 /* remove double spaces from the string because removing ([{}])
659 tends to create those */
660 for(p = new_string + 1; *p != '\0'; p++) {
661 c = (guchar) *p;
662 if(c == ' ') {
663 tmp_p = p - 1;
664 if( *tmp_p == ' ') {
665 len = shrink_string(new_string, p - new_string, len);
666 p--;
671 /* make sure first char isn't a space */
672 if(new_string[0] == ' ')
673 len = shrink_string(new_string, 0, len);
675 /* make sure there isn't a trailing space*/
676 if(new_string[len - 1] == ' ')
677 len--;
679 new_string[len] = '\0';
681 result = g_malloc (len + unacceptable * 2 + 1);
683 /*time to create the escaped string*/
684 for (q = result, p = new_string; *p != '\0'; p++)
686 c = (guchar) *p;
688 if (!ACCEPTABLE (c)) {
689 *q++ = '%'; /* means hex coming */
690 *q++ = hex[c >> 4];
691 *q++ = hex[c & 15];
693 else
694 *q++ = *p;
697 *q = '\0';
699 g_free(new_string);
701 return result;
704 static int shrink_string(gchar *string, int start, int end)
706 int i;
708 for( i = start; i < end; i++)
709 string[i] = string[i + 1];
711 end--;
713 return end;