#include "common.h" #include #include #include #include #include "customcellrendererstate.h" #include "customcellrendererprogress.h" #include "customcellrendererrate.h" #include "customcellrendererleft.h" typedef enum { COLUMN_STATE, COLUMN_TITLE, COLUMN_DOWNLOADED, COLUMN_SEEDED, COLUMN_UP, COLUMN_DOWN, COLUMN_LEFT, COLUMN_PROGRESSSORT, } column_t; typedef struct { state_t state; gchar* title; gfloat downloaded; /* percent */ gfloat seeded; /* ratio */ gfloat down, up; /* kilo (1000) byte per second */ guint64 size; /* bytes */ gint64 left; /* seconds left until done, < 0 means unknown */ } torrent_t; static void torrent_update(torrent_t* torrent, GtkListStore* store, GtkTreeIter* iter); typedef struct { GAsyncQueue* queue; gboolean connected; GtkListStore* liststore; GtkWidget* top; GtkWidget* connectmenuitem, * disconnectmenuitem; GtkWidget* connectdlg; GtkWidget* connectdlg_url; GtkWidget* connectdlg_user; GtkWidget* connectdlg_pwd; GtkWidget* statusbar; guint status_contextid; GKeyFile* config; gchar* configfile; guint config_timer; guint config_try; } master_t; typedef struct { GAsyncQueue* queue; master_t* master; } worker_data_t; typedef enum { MSG_CONNECT, /* main -> worker */ MSG_CONNECTRESULT, /* worker -> main */ MSG_DISCONNECT, /* main -> worker */ MSG_QUIT, /* main -> worker */ MSG_ERROR, /* worker -> main */ MSG_STATUS, /* worker -> main */ } msg_type_t; typedef struct { msg_type_t type; master_t* master; } msg_t; typedef struct { msg_t base; gchar* url; gchar* auth_username, * auth_password; } msg_connect_t; typedef struct { msg_t base; gboolean success; gchar* errmsg; } msg_connectresult_t; typedef struct { msg_t base; gchar* msg; } msg_error_t; typedef struct { msg_t base; gchar* msg; } msg_status_t; typedef struct { msg_t base; } msg_quit_t; typedef struct { msg_t base; } msg_disconnect_t; static void msg_free(msg_t* msg); static msg_t* msg_connect(const gchar* url, const gchar* username, const gchar* password); static msg_t* msg_connectresult(gboolean success, const gchar* errfmt, ...); static msg_t* msg_error(const gchar* format, ...); static msg_t* msg_status(const gchar* format, ...); static msg_t* msg_disconnect(void); static msg_t* msg_quit(void); static void do_connect(GtkMenuItem* menuitem, gpointer data); static void do_disconnect(GtkMenuItem* menuitem, gpointer data); static gpointer worker_main(gpointer data); static gboolean incoming_msg(gpointer data); static gboolean sync_config(gpointer data); static void save_config(master_t* master); static void status(master_t* master, const char* format, ...); int main(int argc, char** argv) { const gchar* gladefile; GtkBuilder* builder; GError* error = NULL; guint ret; GtkWidget* listview; GtkCellRenderer* cell; GtkTreeViewColumn* column; GThread* worker; worker_data_t worker_data; master_t master; memset(&master, 0, sizeof(master)); g_thread_init(NULL); gtk_init(&argc, &argv); worker_data.queue = g_async_queue_new(); master.queue = worker_data.queue; worker_data.master = &master; worker = g_thread_create(worker_main, &worker_data, TRUE, NULL); if (worker == NULL) { fprintf(stderr, "Unable to create thread\n"); return EXIT_FAILURE; } builder = gtk_builder_new(); gladefile = DATAROOTDIR "viewtorrents/viewtorrents.glade"; ret = gtk_builder_add_from_file(builder, gladefile, &error); #ifdef DEBUG if (ret == 0) { g_clear_error(&error); gladefile = "gui/viewtorrents.glade"; ret = gtk_builder_add_from_file(builder, gladefile, &error); } #endif if (ret == 0) { fprintf(stderr, "Unable to load %s: %s\n", gladefile, error->message); g_clear_error(&error); return EXIT_FAILURE; } master.top = GTK_WIDGET(gtk_builder_get_object(builder, "main")); g_signal_connect(master.top, "delete-event", G_CALLBACK(gtk_main_quit), NULL); g_signal_connect(gtk_builder_get_object(builder, "quitmenuitem"), "activate", G_CALLBACK(gtk_main_quit), NULL); master.connectmenuitem = GTK_WIDGET(gtk_builder_get_object(builder, "connectmenuitem")); master.disconnectmenuitem = GTK_WIDGET(gtk_builder_get_object(builder, "disconnectmenuitem")); g_signal_connect(master.connectmenuitem, "activate", G_CALLBACK(do_connect), &master); g_signal_connect(master.disconnectmenuitem, "activate", G_CALLBACK(do_disconnect), &master); gtk_widget_set_sensitive(master.connectmenuitem, TRUE); gtk_widget_set_sensitive(master.disconnectmenuitem, FALSE); listview = GTK_WIDGET(gtk_builder_get_object(builder, "treeview")); master.liststore = GTK_LIST_STORE(gtk_builder_get_object(builder, "liststore")); cell = custom_cell_renderer_state_new(); column = GTK_TREE_VIEW_COLUMN(gtk_builder_get_object(builder, "statecolumn")); gtk_tree_view_column_pack_start(column, cell, TRUE); gtk_tree_view_column_add_attribute(column, cell, "state", COLUMN_STATE); cell = custom_cell_renderer_progress_new(); column = GTK_TREE_VIEW_COLUMN(gtk_builder_get_object(builder, "progresscolumn")); gtk_tree_view_column_pack_start(column, cell, TRUE); gtk_tree_view_column_add_attribute(column, cell, "downloaded", COLUMN_DOWNLOADED); gtk_tree_view_column_add_attribute(column, cell, "seeded", COLUMN_SEEDED); cell = custom_cell_renderer_rate_new(); column = GTK_TREE_VIEW_COLUMN(gtk_builder_get_object(builder, "upcolumn")); gtk_tree_view_column_pack_start(column, cell, TRUE); gtk_tree_view_column_add_attribute(column, cell, "rate", COLUMN_UP); column = GTK_TREE_VIEW_COLUMN(gtk_builder_get_object(builder, "downcolumn")); gtk_tree_view_column_pack_start(column, cell, TRUE); gtk_tree_view_column_add_attribute(column, cell, "rate", COLUMN_DOWN); cell = custom_cell_renderer_left_new(); column = GTK_TREE_VIEW_COLUMN(gtk_builder_get_object(builder, "leftcolumn")); gtk_tree_view_column_pack_start(column, cell, TRUE); gtk_tree_view_column_add_attribute(column, cell, "left", COLUMN_LEFT); master.statusbar = GTK_WIDGET(gtk_builder_get_object(builder, "statusbar")); master.status_contextid = gtk_statusbar_get_context_id(GTK_STATUSBAR(master.statusbar), "main"); master.config = g_key_file_new(); master.configfile = g_build_filename(g_get_user_config_dir(), "viewtorrent.ini", NULL); g_key_file_load_from_file(master.config, master.configfile, G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, NULL); gtk_widget_show_all(master.top); master.connectdlg = GTK_WIDGET(gtk_builder_get_object(builder, "connectdlg")); master.connectdlg_url = GTK_WIDGET(gtk_builder_get_object(builder, "connectdlg_url")); master.connectdlg_user = GTK_WIDGET(gtk_builder_get_object(builder, "connectdlg_user")); master.connectdlg_pwd = GTK_WIDGET(gtk_builder_get_object(builder, "connectdlg_pwd")); if (g_key_file_get_boolean(master.config, "connect", "auto", NULL)) { gchar* url = g_key_file_get_string(master.config, "connect", "url", NULL); gchar* user = g_key_file_get_string(master.config, "connect", "username", NULL); gchar* pass = NULL; if (url != NULL && user != NULL) { GtkWidget* dlg, * label, * pwd; gchar* format, * text; dlg = GTK_WIDGET(gtk_builder_get_object(builder, "pwddlg")); label = GTK_WIDGET(gtk_builder_get_object(builder, "pwddlg_label")); pwd = GTK_WIDGET(gtk_builder_get_object(builder, "pwddlg_pwd")); format = g_strdup(gtk_label_get_text(GTK_LABEL(label))); text = g_strdup_printf(format, url); g_free(format); gtk_label_set_text(GTK_LABEL(label), text); g_free(text); switch (gtk_dialog_run(GTK_DIALOG(dlg))) { case GTK_RESPONSE_OK: pass = g_strdup(gtk_entry_get_text(GTK_ENTRY(pwd))); break; case GTK_RESPONSE_CANCEL: default: g_free(url); url = NULL; } gtk_widget_hide(dlg); } if (url != NULL) { g_async_queue_push(worker_data.queue, msg_connect(url, user, pass)); status(&master, "Autoconnecting to %s...", url); } g_free(url); g_free(user); g_free(pass); } gtk_main(); if (master.config_timer > 0) { g_source_remove(master.config_timer); master.config_timer = 0; sync_config(&master); } g_async_queue_push(worker_data.queue, msg_quit()); g_async_queue_unref(worker_data.queue); g_thread_join(worker); g_key_file_free(master.config); g_free(master.configfile); return EXIT_SUCCESS; } gboolean sync_config(gpointer _data) { master_t* master = (master_t*)_data; gsize length; gchar *data = g_key_file_to_data(master->config, &length, NULL); GError* err = NULL; if (!g_file_set_contents(master->configfile, data, length, &err)) { g_free(data); status(master, "Error writing configfile: %s", err->message); g_error_free(err); master->config_try++; if (master->config_try == 2) { /* Wait longer */ master->config_timer = g_timeout_add_seconds(5 * 60, sync_config, master); return FALSE; } if (master->config_try == 10) { /* Just give up */ return FALSE; } /* Try again */ return TRUE; } g_free(data); master->config_timer = 0; master->config_try = 0; return FALSE; } gboolean incoming_msg(gpointer data) { msg_t* msg = data; switch (msg->type) { case MSG_CONNECTRESULT: { msg_connectresult_t* m = (msg_connectresult_t*)msg; if (m->success) { msg->master->connected = TRUE; gtk_widget_set_sensitive(msg->master->connectmenuitem, FALSE); gtk_widget_set_sensitive(msg->master->disconnectmenuitem, TRUE); g_key_file_set_boolean(msg->master->config, "connect", "auto", TRUE); save_config(msg->master); status(master, "Connected."); } else { GtkWidget* dlg; msg->master->connected = FALSE; gtk_widget_set_sensitive(msg->master->connectmenuitem, TRUE); gtk_widget_set_sensitive(msg->master->disconnectmenuitem, FALSE); gtk_list_store_clear(msg->master->liststore); dlg = gtk_message_dialog_new(GTK_WINDOW(msg->master->top), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "Failed to connect: %s", m->errmsg); gtk_dialog_run(GTK_DIALOG(dlg)); gtk_widget_destroy(dlg); } break; } case MSG_ERROR: { msg_error_t* m = (msg_error_t*)msg; GtkWidget* dlg; dlg = gtk_message_dialog_new(GTK_WINDOW(msg->master->top), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "Fatal error: %s", m->msg); gtk_dialog_run(GTK_DIALOG(dlg)); gtk_widget_destroy(dlg); gtk_main_quit(); break; } case MSG_STATUS: { msg_status_t* m = (msg_status_t*)msg; status(msg->master, m->msg); break; } case MSG_CONNECT: case MSG_DISCONNECT: case MSG_QUIT: g_assert(FALSE); } msg_free(msg); return FALSE; } void do_connect(GtkMenuItem* menuitem, gpointer data) { master_t* master = data; gchar* url, * user, * pass = NULL; if (master->connected) { return; } url = g_key_file_get_string(master->config, "connect", "url", NULL); user = g_key_file_get_string(master->config, "connect", "username", NULL); gtk_entry_set_text(GTK_ENTRY(master->connectdlg_url), url ? url : ""); gtk_entry_set_text(GTK_ENTRY(master->connectdlg_user),user ? user : ""); gtk_entry_set_text(GTK_ENTRY(master->connectdlg_pwd), ""); switch (gtk_dialog_run(GTK_DIALOG(master->connectdlg))) { case GTK_RESPONSE_OK: g_free(url); g_free(user); url = g_strdup(gtk_entry_get_text(GTK_ENTRY(master->connectdlg_url))); user = g_strdup(gtk_entry_get_text(GTK_ENTRY(master->connectdlg_user))); pass = g_strdup(gtk_entry_get_text(GTK_ENTRY(master->connectdlg_pwd))); break; case GTK_RESPONSE_CANCEL: default: g_free(url); g_free(user); url = NULL; } gtk_widget_hide(master->connectdlg); if (url == NULL) { return FALSE; } if (strlen(user) == 0 && strlen(pass) == 0) { g_free(user); g_free(pass); user = NULL; pass = NULL; } gtk_widget_set_sensitive(master->connectmenuitem, FALSE); status(master, "Connecting to %s...", url); g_key_file_set_string(master->config, "connect", "url", url); if (user != NULL) { g_key_file_set_string(master->config, "connect", "username", user); } else { g_key_file_remove_key(master->config, "connect", "username", NULL); } g_key_file_remove_key(master->config, "connect", "auto", NULL); save_config(master); g_async_queue_push(master->queue, msg_connect(url, user, pass)); g_free(url); g_free(user); g_free(pass); } void do_disconnect(GtkMenuItem* menuitem, gpointer data) { master_t* master = data; if (!master->connected) { return; } gtk_widget_set_sensitive(master->disconnectmenuitem, FALSE); status(master, "Disconnecting..."); g_async_queue_push(master->queue, msg_disconnect()); } void save_config(master_t* master) { if (master->config_timer == 0) { master->config_timer = g_timeout_add_seconds(5, sync_config, master); } } void status(master_t* master, const char* format, ...) { gchar* tmp; va_list args; gtk_statusbar_pop(GTK_STATUSBAR(master->statusbar), master->status_contextid); va_start(args, format); tmp = g_strdup_vprintf(format, args); va_end(args); gtk_statusbar_push(GTK_STATUSBAR(master->statusbar), master->status_contextid, tmp); g_free(tmp); } void torrent_update(torrent_t* torrent, GtkListStore* store, GtkTreeIter* iter) { gtk_list_store_set(store, iter, COLUMN_STATE, torrent->state, COLUMN_TITLE, torrent->title, COLUMN_DOWNLOADED, torrent->downloaded, COLUMN_SEEDED, torrent->seeded, COLUMN_UP, torrent->up, COLUMN_DOWN, torrent->down, COLUMN_LEFT, torrent->left, COLUMN_PROGRESSSORT, torrent->downloaded < 100.0f ? torrent->downloaded : torrent->seeded + 1000.0f, -1); } static void worker_respond(worker_data_t* data, msg_t* msg); gpointer worker_main(gpointer _data) { worker_data_t* data = _data; xmlrpc_env env; xmlrpc_client* client; xmlrpc_server_info* server = NULL; gboolean quit = FALSE; data->queue = g_async_queue_ref(data->queue); xmlrpc_env_init(&env); xmlrpc_client_setup_global_const(&env); xmlrpc_client_create(&env, XMLRPC_CLIENT_NO_FLAGS, "viewtorrents", "0.1", NULL, 0, &client); if (env.fault_occurred) { worker_respond(data, msg_error("Unable to create XMLRPC-C client (%d): %s", env.fault_code, env.fault_string)); quit = TRUE; } while (!quit) { msg_t* msg = g_async_queue_pop(data->queue); switch (msg->type) { case MSG_CONNECT: { const gchar* tmp; xmlrpc_value* params, * result; msg_connect_t* m = (msg_connect_t*)msg; server = xmlrpc_server_info_new(&env, m->url); if (m->auth_username != NULL) { xmlrpc_server_info_set_user(&env, server, m->auth_username, m->auth_password); xmlrpc_server_info_allow_auth_basic(&env, server); } params = xmlrpc_array_new(&env); xmlrpc_client_call2(&env, client, server, "system.client_version", params, &result); if (env.fault_occurred) { xmlrpc_DECREF(params); xmlrpc_server_info_free(server); msg_free(msg); server = NULL; worker_respond(data, msg_connectresult(FALSE, "Unable to get client version (%d): %s", env.fault_code, env.fault_string)); continue; } xmlrpc_DECREF(params); xmlrpc_read_string(&env, result, &tmp); if (env.fault_occurred) { xmlrpc_DECREF(result); xmlrpc_server_info_free(server); msg_free(msg); server = NULL; worker_respond(data, msg_connectresult(FALSE, "Unable to get client version (%d): %s", env.fault_code, env.fault_string)); continue; } printf("result: %s\n", tmp); xmlrpc_DECREF(result); worker_respond(data, msg_connectresult(TRUE, NULL)); msg_free(msg); break; } case MSG_DISCONNECT: { xmlrpc_server_info_free(server); server = NULL; worker_respond(data, msg_status("Disconnected.")); break; } case MSG_QUIT: quit = TRUE; break; case MSG_CONNECTRESULT: case MSG_ERROR: case MSG_STATUS: g_assert(FALSE); break; } } if (server != NULL) { xmlrpc_server_info_free(server); } xmlrpc_client_destroy(client); xmlrpc_client_teardown_global_const(); g_async_queue_unref(data->queue); return NULL; } void worker_respond(worker_data_t* data, msg_t* msg) { msg->master = data->master; g_main_context_invoke(NULL, incoming_msg, msg); } void msg_free(msg_t* msg) { switch (msg->type) { case MSG_CONNECT: { msg_connect_t* m = (msg_connect_t*)msg; g_free(m->url); g_free(m->auth_username); g_free(m->auth_password); break; } case MSG_CONNECTRESULT: { msg_connectresult_t* m = (msg_connectresult_t*)msg; g_free(m->errmsg); break; } case MSG_ERROR: { msg_error_t* m = (msg_error_t*)msg; g_free(m->msg); break; } case MSG_STATUS: { msg_status_t* m = (msg_status_t*)msg; g_free(m->msg); break; } case MSG_DISCONNECT: case MSG_QUIT: break; } g_free(msg); } msg_t* msg_connect(const gchar* url, const gchar* username, const gchar* password) { msg_connect_t* msg = g_new0(msg_connect_t, 1); msg->base.type = MSG_CONNECT; msg->url = strdup(url); msg->auth_username = g_strdup(username); msg->auth_password = g_strdup(password); return &msg->base; } msg_t* msg_connectresult(gboolean success, const gchar* errfmt, ...) { msg_connectresult_t* msg = g_new0(msg_connectresult_t, 1); msg->base.type = MSG_CONNECTRESULT; if (success) { msg->success = TRUE; } else { va_list args; msg->success = FALSE; va_start(args, errfmt); msg->errmsg = g_strdup_vprintf(errfmt, args); va_end(args); } return &msg->base; } msg_t* msg_error(const gchar* format, ...) { msg_error_t* msg = g_new0(msg_error_t, 1); va_list args; msg->base.type = MSG_ERROR; va_start(args, format); msg->msg = g_strdup_vprintf(format, args); va_end(args); return &msg->base; } msg_t* msg_status(const gchar* format, ...) { msg_status_t* msg = g_new0(msg_status_t, 1); va_list args; msg->base.type = MSG_STATUS; va_start(args, format); msg->msg = g_strdup_vprintf(format, args); va_end(args); return &msg->base; } msg_t* msg_disconnect(void) { msg_disconnect_t* msg = g_new0(msg_disconnect_t, 1); msg->base.type = MSG_DISCONNECT; return &msg->base; } msg_t* msg_quit(void) { msg_quit_t* msg = g_new0(msg_quit_t, 1); msg->base.type = MSG_QUIT; return &msg->base; }