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"
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();
46 * Used by gmpc to check against what version the plugin is compiled
48 int plugin_api_version
= PLUGIN_API_VERSION
;
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);
59 * Preferences structure
61 gmpcPrefPlugin lyrics_gpp
= {
62 .construct
= lyrics_construct
,
63 .destroy
= lyrics_destroy
69 gmpcMetaDataPlugin lyric_fetch
= {
70 .get_priority
= fetch_priority
,
71 .get_image
= fetch_lyric
76 .name
= "Lyrics fetcher",
78 .version
= {PLUGIN_MAJOR_VERSION
,PLUGIN_MINOR_VERSION
,PLUGIN_MICRO_VERSION
},
80 .plugin_type
= GMPC_PLUGIN_META_DATA
,
81 /** initialize function */
83 /** Preferences structure */
85 /** Meta data structure */
86 .metadata
= &lyric_fetch
,
88 .get_enabled
= lyrics_get_enabled
,
90 .set_enabled
= lyrics_set_enabled
93 static struct lyrics_api apis
[] =
96 "http://lyricwiki.org/server.php",
103 __lyricwiki_get_soap_message
,
104 __lyricwiki_get_soap_lyrics
107 "http://api.leoslyrics.com/",
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",
113 __leoslyrics_get_lyrics
,
118 "http://lyrictracker.com/soap.php?cln=" PLUGIN_AUTH
"&clv=undef",
120 "&act=query&ar=%s&ti=%s",
123 __lyrictracker_get_id
,
124 __lyrictracker_get_lyrics
,
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() {
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
);
148 void init_post_message(post_message
*s
) {
153 s
->response_code
= -1;
155 /* the only thing not automagically freed is url */
156 void free_post_message(post_message
*s
) {
158 g_string_free(s
->body
, TRUE
);
159 if(s
->response
!=NULL
)
160 g_string_free(s
->response
, TRUE
);
162 curl_slist_free_all(s
->headers
);
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))
180 xmlNodePtr
get_node_by_name (xmlNodePtr node
, xmlChar
*name
)
184 for (cur
= node
; cur
; cur
= cur
->next
)
185 if (xmlStrEqual(cur
->name
, name
) &&
186 (cur
->type
== XML_ELEMENT_NODE
))
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
;
205 gchar
*esc_hid
= NULL
; // esc => escaped
211 if (!api
->get_id
|| !api
->get_lyrics
)
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
);
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
);
235 debug_printf(DEBUG_INFO
, "search url:'%s'\n", search_url
);
237 if (!gmpc_easy_download (search_url
, &dl
))
243 results_doc
= xmlParseMemory(dl
.data
, dl
.size
);
244 gmpc_easy_download_clean(&dl
);
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
);
262 xmlFreeDoc(results_doc
);
265 esc_hid
= __lyrics_process_string(hid
);
268 temp
= g_strdup_printf("%s%s", api
->host
, api
->lyrics_uri
);
269 lyrics_url
= g_strdup_printf(temp
, esc_hid
);
273 if (!gmpc_easy_download (lyrics_url
, &dl
))
283 data
= api
->get_lyrics(&dl
);
285 gmpc_easy_download_clean(&dl
);
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);
301 *lyrics = g_strdup(__STR_NODATA_ERROR);
305 if (!data
|| !strlen(data
))
315 *lyrics
= g_strdup(data
);
316 /* Cleanup and return */
324 /* yeah, I know.. I'm great with names */
325 int do_post (post_message
*msg
) {
329 con
= curl_easy_init();
332 debug_printf(DEBUG_ERROR
, "You really need a url in post_message\n");
336 debug_printf(DEBUG_ERROR
, "You need a body in post_message\n");
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);
352 curl_easy_setopt(con
, CURLOPT_PROXY
, value
);
353 curl_easy_setopt(con
, CURLOPT_PROXYPORT
, port
);
354 cfg_free_string(value
);
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
);
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
);
385 curl_easy_cleanup(con
);
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
) {
400 post_message request
;
403 if (!api
->get_soap_message
|| !api
->get_soap_lyrics
) {
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
);
415 free_post_message(&request
);
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
);
426 free_post_message(&request
);
427 debug_printf(DEBUG_INFO
, "got error from perform()\n");
431 /* now, for the parsing */
432 xml
= xmlParseMemory(request
.response
->str
, request
.response
->len
);
433 free_post_message(&request
);
440 *lyrics
= api
->get_soap_lyrics(xml
, exact
);
450 int fetch_lyric_loop(mpd_Song
*song
, gchar
**lyrics
, int preferred_api
, int exact
) {
452 int id
= preferred_api
;
459 /* if we have something, free it -- it will be replaced */
462 debug_printf(DEBUG_INFO
, "Search API: %s\n", apis
[id
].name
);
466 ret
= fetch_lyrics_soap(song
, &(apis
[id
]), lyrics
, exact
);
468 ret
= fetch_lyrics(song
, &(apis
[id
]), lyrics
, exact
);
470 /* loop through api array */
471 if(id
==preferred_api
&&preferred_api
!=0) {
475 if(++id
==preferred_api
&&apis
[id
].name
!= NULL
)
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
);
485 //destroy_xml_error();
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
;
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);
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
);
512 return META_DATA_AVAILABLE
;
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
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
);
556 * Initialize GTK widgets for the preferences window.
558 void lyrics_construct (GtkWidget
*container
)
560 GtkWidget
*enable_cg
, *label
, *combo
, *match
;
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";
622 gint unacceptable
= 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
++) {
638 if(c
== '(' || c
== '[' || c
== '{') {
640 } else if(c
== ')' || c
== ']' || c
== '}') {
644 } else if(depth
== 0) {
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
++) {
665 len
= shrink_string(new_string
, p
- new_string
, len
);
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] == ' ')
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
++)
688 if (!ACCEPTABLE (c
)) {
689 *q
++ = '%'; /* means hex coming */
704 static int shrink_string(gchar
*string
, int start
, int end
)
708 for( i
= start
; i
< end
; i
++)
709 string
[i
] = string
[i
+ 1];