/*\ XMMS - Cross-platform multimedia player |*| Copyright (C) 1998-1999 Peter Alm, Mikael Alm, Olle Hallnas, |*| Thomas Nilsson and 4Front Technologies |*| |*| CD audio data input plugin by Willem Monsuwe (willem@stack.nl) |*| |*| 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. \*/ /*\ |*| Functions to call the CDDB server, using the cddp protocol \*/ #include "cdread.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /*\ PROTO \*/ static GtkWidget *proto_win = 0, *proto_box; void proto_win_show(void) { if (!proto_win) { GtkWidget *scroll_win; GtkObject *hadj, *vadj; proto_win = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_signal_connect(GTK_OBJECT(proto_win), "destroy", GTK_SIGNAL_FUNC(gtk_widget_destroyed), &proto_win); gtk_window_set_title(GTK_WINDOW(proto_win), "CDDB protocol"); gtk_window_set_policy(GTK_WINDOW(proto_win), FALSE, TRUE, TRUE); gtk_container_border_width(GTK_CONTAINER(proto_win), 10); hadj = gtk_adjustment_new(0.0, 0.0, 1.0, 0.01, 0.1, 0.1); vadj = gtk_adjustment_new(0.0, 0.0, 1.0, 0.01, 0.1, 0.1); scroll_win = gtk_scrolled_window_new(GTK_ADJUSTMENT(hadj), GTK_ADJUSTMENT(vadj)); gtk_container_add(GTK_CONTAINER(proto_win), scroll_win); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll_win), GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS); gtk_widget_set_usize(scroll_win, 300, 200); proto_box = gtk_vbox_new(FALSE, 0); gtk_scrolled_window_add_with_viewport( GTK_SCROLLED_WINDOW(scroll_win), proto_box); gtk_widget_show(scroll_win); gtk_widget_show(proto_box); } gtk_widget_show(proto_win); } static void proto_win_add(gchar *fmt, ...) { gchar *text; va_list args; GtkWidget *lb; if (!proto_win) return; va_start(args, fmt); text = g_strdup_vprintf(fmt, args); va_end(args); if (text[strlen(text) - 1] == '\n') text[strlen(text) - 1] = 0; GDK_THREADS_ENTER(); lb = gtk_label_new(text); gtk_misc_set_alignment(GTK_MISC(lb), 0.0, 0.5); gtk_label_set_justify(GTK_LABEL(lb), GTK_JUSTIFY_LEFT); gtk_box_pack_start(GTK_BOX(proto_box), lb, TRUE, TRUE, 0); gtk_widget_show(lb); GDK_THREADS_LEAVE(); g_free(text); } /*\ PROTO \*/ void show_dialog(gchar *fmt, ...) { gchar *text; va_list args; va_start(args, fmt); text = g_strdup_vprintf(fmt, args); va_end(args); GDK_THREADS_ENTER(); xmms_show_message("CDDB Error", text, "Ok", FALSE, NULL, NULL); GDK_THREADS_LEAVE(); g_free(text); } struct var_val { volatile int *var; int val; }; static void choice_button_cb(GtkWidget *w, gpointer data) { struct var_val *vv = (struct var_val *)data; *vv->var = vv->val; } /*\ Show a dialog with a list of choice buttons, return chosen button |*| (Cancel button returns -1) \*/ int choice_dialog(gchar *text, gchar **list, int n) { int i; GtkWidget *dialog, *vbox, *label, *bbox, *hbox, *button; struct var_val *vv; volatile int choice; if (n == 0) return -1; if ((n == 1) && (!cd_cfg.cddb_choice_one)) return 0; NEW(vv, n + 1); GDK_THREADS_ENTER(); dialog = gtk_dialog_new(); gtk_window_set_title(GTK_WINDOW(dialog), text); vbox = gtk_vbox_new(FALSE, 0); gtk_container_set_border_width(GTK_CONTAINER(vbox), 15); gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), vbox, TRUE, TRUE, 0); label = gtk_label_new(text); gtk_box_pack_start(GTK_BOX(vbox), label, TRUE, TRUE, 0); gtk_widget_show(label); gtk_widget_show(vbox); bbox = gtk_vbutton_box_new(); gtk_button_box_set_layout(GTK_BUTTON_BOX(bbox), GTK_BUTTONBOX_SPREAD); gtk_button_box_set_spacing(GTK_BUTTON_BOX(bbox), 5); gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->action_area), bbox, FALSE, FALSE, 0); choice = n; for (i = 0; i < n; i++) if (list[i]) { vv[i].var = &choice; vv[i].val = i; button = gtk_button_new_with_label(list[i]); gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(choice_button_cb), (gpointer)&vv[i]); gtk_signal_connect_object(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(gtk_widget_destroy), GTK_OBJECT(dialog)); gtk_box_pack_start(GTK_BOX(bbox), button, FALSE, FALSE, 0); GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT); if (i == 0) gtk_widget_grab_default(button); gtk_widget_show(button); } vv[n].var = &choice; vv[n].val = -1; hbox = gtk_hbutton_box_new(); gtk_button_box_set_layout(GTK_BUTTON_BOX(hbox), GTK_BUTTONBOX_END); gtk_button_box_set_spacing(GTK_BUTTON_BOX(hbox), 5); gtk_box_pack_start(GTK_BOX(bbox), hbox, FALSE, FALSE, 0); button = gtk_button_new_with_label("Cancel"); gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(choice_button_cb), (gpointer)&vv[n]); gtk_signal_connect_object(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(gtk_widget_destroy), GTK_OBJECT(dialog)); gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0); GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT); gtk_widget_show(button); gtk_widget_show(hbox); gtk_widget_show(bbox); gtk_widget_show(dialog); GDK_THREADS_LEAVE(); while (choice == n) xmms_usleep(10000); g_free(vv); return choice; } /*\ TCP utility functions \*/ static gint tcp_connect(char *host, gint port) { gint sock; struct hostent *he; struct sockaddr_in sin; he = gethostbyname(host); if (!he) { show_dialog("Couldn't lookup CDDB server:\n(%s)\n%s", host, hstrerror(h_errno)); return -1; } memcpy(&sin.sin_addr.s_addr, he->h_addr, sizeof(sin.sin_addr.s_addr)); sin.sin_family = he->h_addrtype; sin.sin_port = htons(port); sock = socket(sin.sin_family, SOCK_STREAM, 0); if (sock < 0) { show_dialog("Couldn't create socket:\n%s", g_strerror(errno)); return -1; } if (connect(sock, (struct sockaddr *)&sin, sizeof(sin)) < 0) { show_dialog("Couldn't connect to CDDB server:\n(%s:%d)\n%s", host, port, g_strerror(errno)); close(sock); return -1; } fcntl(sock, F_SETFL, O_NONBLOCK); proto_win_add("--- tcp: Connected to %s:%d ---\n", host, port); return sock; } struct cddb_req { gchar *filename; gchar *query; gchar *device; gchar rest[256]; gboolean del; gint sock; }; static gint going = 1, running = 0; #define READ_USEC 100000 #define READ_TIMEOUT ((10 * 1000000) / READ_USEC) /*\ Read one line from the socket, not including the newline \*/ static gchar * read_line(struct cddb_req *req) { gchar *ret = 0; int rtr = 0; while (going) { int r; gchar *p = strchr(req->rest, '\n'); if (p) { *p++ = 0; if (req->rest[strlen(req->rest) - 1] == '\r') req->rest[strlen(req->rest) - 1] = 0; STRCAT(ret, req->rest); g_memmove(req->rest, p, strlen(p) + 1); proto_win_add("%s\n", ret); return ret; } STRCAT(ret, req->rest); r = read(req->sock, req->rest, sizeof(req->rest) - 1); if (r < 0) { req->rest[0] = 0; if (errno == EAGAIN) { errno = ETIMEDOUT; if (++rtr < READ_TIMEOUT) { xmms_usleep(READ_USEC); continue; } } if (ret) g_free(ret); show_dialog( "Couldn't read from CDDB server:\n%s", g_strerror(errno)); return NULL; } rtr = 0; req->rest[r] = 0; } if (ret) g_free(ret); return NULL; } /*\ Write one line to the socket, g_free()ing the line afterwards \*/ static gint write_line(gint sock, gchar *l) { gchar *p = l; int rtr = 0; proto_win_add("%s", l); while (*p && going) { gint w = write(sock, p, strlen(p)); if (w < 0) { if (errno == EAGAIN) { errno = ETIMEDOUT; if (++rtr < READ_TIMEOUT) { xmms_usleep(READ_USEC); continue; } } show_dialog( "Couldn't write to CDDB server:\n%s", g_strerror(errno)); return -1; } rtr = 0; p += w; } g_free(l); return (p - l); } static void * end_req(struct cddb_req *req) { if (req->sock >= 0) close(req->sock); cddb_reread(); g_free(req->filename); g_free(req->device); g_free(req->query); --running; return NULL; } /*\ Returns read command \*/ static gchar * cddb_parse_query(struct cddb_req *req) { gchar *did, *p, *ctg, *l; l = read_line(req); if (!l) return NULL; p = l + 3; if (!memcmp(l, "211", 3)) { gchar **ll = 0; int c, ls = 0; while (1) { gchar *tmp; tmp = read_line(req); if (!tmp) break; if (tmp[0] == '.') { STRCAT(l, ".\n"); g_free(tmp); break; } RENEW(ll, ls + 1); ll[ls] = tmp; ls++; STRCAT(l, "\n"); STRCAT(l, tmp); } c = -1; if (ls) { c = choice_dialog("Inexact matches found. Make a choice:", ll, ls); } else { show_dialog("CDDB server found NO inexact matches:\n" "%s", l); } if (c >= 0) { /*\ Put match into l (Trick: This sets p as well) \*/ p = ll[c]; ll[c] = l; l = p; p--; } while (--ls >= 0) g_free(ll[ls]); g_free(ll); if (c < 0) { g_free(l); return NULL; } } else if (memcmp(l, "200", 3)) { show_dialog("Couldn't query from CDDB server:\n%s", l); g_free(l); return NULL; } while (isspace(*++p)) ; ctg = p; while (*++p && !isspace(*p)) ; *p = 0; ctg = g_strdup(ctg); while (isspace(*++p)) ; did = p; while (!isspace(*++p)) ; *p = 0; /*\ Blatantly assume the last 8 characters of the filename |*| to be the requested discid \*/ p = req->filename; p += strlen(p) - 8; if (memcmp(did, p, 8)) { char *tmp; /*\ Make a softlink \*/ tmp = g_strdup(req->filename); memcpy(p, did, 8); if (req->del) unlink(tmp); if (symlink(p, tmp) < 0) { show_dialog( "Couldn't link CDDB file:\n" "(%s -> %s)\n%s", tmp, req->filename, g_strerror(errno)); g_free(l); return NULL; } g_free(tmp); } g_free(l); l = g_strdup_printf("cddb read %s %s\n", ctg, p); g_free(ctg); return l; } static void * cddb_save_query(struct cddb_req *req) { gint fd; FILE *f; gchar *l; l = read_line(req); if (!l) return end_req(req); if (l[0] != '2') { show_dialog("Couldn't get entry from CDDB server:\n%s", l); return end_req(req); } g_free(l); if (req->del) unlink(req->filename); fd = open(req->filename, O_WRONLY|O_CREAT|O_EXCL, 0666); if (fd < 0) { show_dialog("Couldn't create CDDB file (%s):\n%s", req->filename, g_strerror(errno)); return end_req(req); } f = fdopen(fd, "w"); while (1) { l = read_line(req); if (!l) break; if (l[0] == '.') { g_free(l); break; } fputs(l, f); fputc('\n', f); g_free(l); } if (fclose(f) < 0) show_dialog("Couldn't write to CDDB file (%s):\n%s", req->filename, g_strerror(errno)); return end_req(req); } static gchar * make_http_get(gchar *query) { gchar *p = query; struct utsname uts; /*\ Substitute spaces with pluses \*/ while (*p) { if (isspace(*p)) *p = '+'; p++; } uname(&uts); p = g_strdup_printf("GET %s?cmd=%s&hello=%s+%s+" PACKAGE "+" VERSION "&proto=1 HTTP/1.0\n\n", cd_cfg.cddb_cgi, query, g_get_user_name(), uts.nodename); g_free(query); return p; } static void * cddbp_query_thread(void *arg) { struct cddb_req *req = (struct cddb_req *)arg; struct utsname uts; gchar *l; req->sock = tcp_connect(cd_cfg.cddb_server, cd_cfg.cddb_port); if (req->sock < 0) return end_req(req); req->rest[0] = 0; l = read_line(req); if (!l) return end_req(req); if (l[0] != '2') { show_dialog("CDDB Server didn't want to connect:\n%s", l); g_free(l); return end_req(req); } g_free(l); uname(&uts); l = g_strdup_printf("cddb hello %s %s " PACKAGE " " VERSION "\n", g_get_user_name(), uts.nodename); if (write_line(req->sock, l) < 0) return end_req(req); l = read_line(req); if (!l) return end_req(req); if (l[0] != '2') { show_dialog("Couldn't shake hands with CDDB server:\n%s", l); g_free(l); return end_req(req); } g_free(l); l = req->query; req->query = 0; if (write_line(req->sock, l) < 0) return end_req(req); l = cddb_parse_query(req); if (!l) return end_req(req); if (write_line(req->sock, l) < 0) return end_req(req); return cddb_save_query(req); } static void * http_query_thread(void *arg) { struct cddb_req *req = (struct cddb_req *)arg; gchar *l; req->sock = tcp_connect(cd_cfg.cddb_server, cd_cfg.cddb_port); if (req->sock < 0) return end_req(req); req->rest[0] = 0; if (write_line(req->sock, make_http_get(g_strdup(req->query))) < 0) return end_req(req); l = read_line(req); if (!l) return end_req(req); if (isdigit(l[0])) { /*\ Oops, we seem to be connected to a CDDBP port.. \*/ g_free(l); close(req->sock); return cddbp_query_thread(req); } /*\ Eat up the HTTP header \*/ while (strlen(l) > 0) { g_free(l); l = read_line(req); if (!l) return end_req(req); } g_free(l); l = cddb_parse_query(req); if (!l) return end_req(req); close(req->sock); req->sock = tcp_connect(cd_cfg.cddb_server, cd_cfg.cddb_port); if (req->sock < 0) return end_req(req); req->rest[0] = 0; if (write_line(req->sock, make_http_get(l)) < 0) return end_req(req); l = read_line(req); if (!l) return end_req(req); /*\ Eat up the HTTP header \*/ while (strlen(l) > 0) { g_free(l); l = read_line(req); if (!l) return end_req(req); } g_free(l); return cddb_save_query(req); } void cddb_server_cleanup(void) { going = 0; while (running > 0) xmms_usleep(10000); } static gchar * make_query(struct cd_struct *cd) { char *p, tmp[1024]; /*\ Surely enough for 100 tracks \*/ int i; strcpy(tmp, "cddb query "); p = tmp + strlen(tmp); sprintf(p, "%08x ", cd->id); p += strlen(p); sprintf(p, "%u ", cd->last_trk - cd->first_trk + 1); p += strlen(p); for (i = cd->first_trk; i <= cd->last_trk; i++) { sprintf(p, "%u ", cd->lba[i]); p += strlen(p); } sprintf(p, "%u\n", (cd->lba[cd->last_trk + 1] / 75) - (cd->lba[cd->first_trk] / 75)); return g_strdup(tmp); } /*\ Send a request to the CDDB server, store under filename 'fn' |*| NB: Assumes the cd_struct is safe, so lock it. |*| This will take control of 'fn' (and free it) \*/ void cddb_server_get(struct cd_struct *cd, gchar *fn, gboolean del) { struct cddb_req *req; pthread_t thread; if (cd->cddb_pending) return; cd->cddb_pending = 1; NEW(req, 1); req->filename = fn; req->device = g_strdup(cd->device); req->query = make_query(cd); req->del = del; ++running; if (pthread_create(&thread, NULL, http_query_thread, req) < 0) { show_dialog("Couldn't start CDDB query thread!:\n%s", g_strerror(errno)); end_req(req); return; } pthread_detach(thread); }