album art support and other work on play window
[devious] / devious.c
index a219344..e83f1ca 100644 (file)
--- a/devious.c
+++ b/devious.c
@@ -1,5 +1,5 @@
 /*
- * Devious: UPNP Control Point for Maemo 5
+ * Devious: UPnP Control Point for Maemo 5
  *
  * Copyright (C) 2009 Kyle Cronan
  *
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <gio/gio.h>
 #include <glib.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
 #include <gtk/gtk.h>
 #include <hildon/hildon.h>
 #include <libgupnp/gupnp-control-point.h>
 #include <libgupnp-av/gupnp-av.h>
+#include <libsoup/soup.h>
 
 #include "devious.h"
 
@@ -58,7 +61,7 @@ void add_content(GUPnPDIDLLiteParser *didl_parser, xmlNode *object_node,
 
 struct browse_data *browse_data_new(GUPnPServiceProxy *content_dir,
                                     const char *id, guint32 starting_index,
-                                    GtkListStore *list)
+                                    GtkListStore *list, struct proxy_set *set)
 {
     struct browse_data *data;
 
@@ -67,6 +70,7 @@ struct browse_data *browse_data_new(GUPnPServiceProxy *content_dir,
     data->id = g_strdup(id);
     data->starting_index = starting_index;
     data->list = list;
+    data->set = set;
 
     return data;
 }
@@ -101,8 +105,9 @@ void browse_cb(GUPnPServiceProxy *content_dir,
         guint32 remaining;
         GError *error = NULL;
 
-        if (!gupnp_didl_lite_parser_parse_didl(data->didl_parser, didl_xml,
-                                               add_content, data, &error)) {
+        if (!gupnp_didl_lite_parser_parse_didl(data->set->app->didl_parser,
+                                               didl_xml, add_content, data,
+                                               &error)) {
             g_warning("%s\n", error->message);
             g_error_free(error);
         }
@@ -115,7 +120,7 @@ void browse_cb(GUPnPServiceProxy *content_dir,
         /* Keep browsing till we get each and every object */
         if (remaining != 0) browse(content_dir, data->id, data->starting_index,
                                    MIN(remaining, MAX_BROWSE),
-                                   data->list, data->didl_parser);
+                                   data->list, data->set);
     } else if (error) {
         GUPnPServiceInfo *info;
 
@@ -132,10 +137,11 @@ void browse_cb(GUPnPServiceProxy *content_dir,
 
 void browse(GUPnPServiceProxy *content_dir, const char *container_id,
             guint32 starting_index, guint32 requested_count,
-            GtkListStore *list, GUPnPDIDLLiteParser *didl_parser)
+            GtkListStore *list, struct proxy_set *proxy_set)
 {
     struct browse_data *data;
-    data = browse_data_new(content_dir, container_id, starting_index, list);
+    data = browse_data_new(content_dir, container_id, starting_index, list,
+                           proxy_set);
 
     gupnp_service_proxy_begin_action(content_dir, "Browse", browse_cb, data,
                                      "ObjectID", G_TYPE_STRING, container_id,
@@ -369,7 +375,7 @@ return_point:
 }
 
 char *find_compat_uri_from_metadata(const char *metadata, char **duration,
-                                    struct proxy *renderer)
+                                    char **art_uri, struct proxy *renderer)
 {
     char *uri = NULL;
     void on_didl_item_available(GUPnPDIDLLiteParser *didl_parser,
@@ -401,13 +407,18 @@ char *find_compat_uri_from_metadata(const char *metadata, char **duration,
             uri = gupnp_didl_lite_property_get_value(res_node);
             *duration = gupnp_didl_lite_property_get_attribute(res_node,
                                                                "duration");
+            GList *album_art =
+                gupnp_didl_lite_object_get_property(item_node, ALBUM_ART);
+            if (album_art) *art_uri =
+                gupnp_didl_lite_property_get_value((xmlNode *)album_art->data);
+            else *art_uri = NULL;
         }
         g_list_free(resources);
     }
 
     GError *error = NULL;
     /* Assumption: metadata only contains a single didl object */
-    gupnp_didl_lite_parser_parse_didl(renderer->set->didl_parser, metadata,
+    gupnp_didl_lite_parser_parse_didl(renderer->set->app->didl_parser, metadata,
                                       on_didl_item_available, NULL, &error);
     if (error) {
         g_warning("%s\n", error->message);
@@ -419,7 +430,7 @@ char *find_compat_uri_from_metadata(const char *metadata, char **duration,
 
 struct proxy *current_renderer(struct proxy_set *proxy_set)
 {
-    int i = hildon_picker_button_get_active(proxy_set->renderer_picker);
+    int i = hildon_picker_button_get_active(proxy_set->app->renderer_picker);
     GtkTreeIter iter;
     gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(proxy_set->renderer_list),
                                   &iter, NULL, i);
@@ -430,6 +441,26 @@ struct proxy *current_renderer(struct proxy_set *proxy_set)
     return g_hash_table_lookup(proxy_set->renderers, udn);
 }
 
+void set_image(SoupSession *session, SoupMessage *msg, gpointer user_data)
+{
+    struct selection_data *data = (struct selection_data *)user_data;
+
+    if (!SOUP_STATUS_IS_SUCCESSFUL(msg->status_code)) return;
+
+    GInputStream *stream =
+        g_memory_input_stream_new_from_data(msg->response_body->data,
+                                            msg->response_body->length, NULL);
+    
+    GError *error = NULL;
+    GdkPixbuf *pixbuf =
+        gdk_pixbuf_new_from_stream_at_scale(stream, PLAY_WINDOW_IMAGE_SIZE,
+                                                    PLAY_WINDOW_IMAGE_SIZE,
+                                            TRUE, NULL, &error);
+    g_object_unref(G_OBJECT(stream));
+
+    if (!error) gtk_image_set_from_pixbuf(GTK_IMAGE(data->art_image), pixbuf);
+}
+
 void set_av_transport_uri(GUPnPServiceProxy *content_dir,
                           GUPnPServiceProxyAction *action, gpointer user_data)
 {
@@ -443,15 +474,20 @@ void set_av_transport_uri(GUPnPServiceProxy *content_dir,
         g_error_free(error);
     }
 
-    struct proxy_set *proxy_set = (struct proxy_set *)user_data;
-    struct proxy *renderer = current_renderer(proxy_set);
+    struct selection_data *data = (struct selection_data *)user_data;
+    struct proxy *renderer = current_renderer(data->set);
 
-    char *duration;
-    char *uri = find_compat_uri_from_metadata(metadata, &duration, renderer);
+    char *uri = find_compat_uri_from_metadata(metadata, &data->duration,
+                                              &data->art_uri, renderer);
     if (!uri) {
         g_warning("no compatible URI found.");
         return;
     }
+    
+    if (data->art_uri) {
+        SoupMessage *msg = soup_message_new("GET", data->art_uri);
+        soup_session_queue_message(data->set->app->soup, msg, set_image, data);
+    }
 
     GUPnPServiceProxy *av_transport = GUPNP_SERVICE_PROXY(
         gupnp_device_info_get_service(GUPNP_DEVICE_INFO(renderer->proxy),
@@ -520,12 +556,10 @@ void av_transport_send_action(GUPnPServiceProxy *av_transport, char *action,
     g_hash_table_unref(args);
 }
 
-void transport_selection(struct proxy *server, GtkTreeRowReference *row)
+void transport_selection(struct proxy *server, struct selection_data *data)
 {
-    server->current_selection = row;
-    
-    GtkTreeModel *model = gtk_tree_row_reference_get_model(row);
-    GtkTreePath *path = gtk_tree_row_reference_get_path(row);
+    GtkTreeModel *model = gtk_tree_row_reference_get_model(data->row);
+    GtkTreePath *path = gtk_tree_row_reference_get_path(data->row);
 
     GtkTreeIter iter;
     gtk_tree_model_get_iter(model, &iter, path);
@@ -537,7 +571,7 @@ void transport_selection(struct proxy *server, GtkTreeRowReference *row)
                                      COL_CONTENT, &content, COL_ID, &id, -1);
     
     gupnp_service_proxy_begin_action(content, "Browse",
-                                         set_av_transport_uri, server->set,
+                                         set_av_transport_uri, data,
                                      "ObjectID", G_TYPE_STRING, id,
                                      "BrowseFlag", G_TYPE_STRING,
                                          "BrowseMetadata",
@@ -559,7 +593,92 @@ void play_button(GtkWidget *button, gpointer data)
     av_transport_send_action(av_transport, "Play", args);
 }
 
-GtkWidget *play_window(struct proxy *server, GtkTreeRowReference *row)
+struct selection_data *selection_data_new(struct proxy *server,
+                                          GtkTreeModel *model,
+                                          GtkTreePath *path)
+{
+    struct selection_data *data =
+        (struct selection_data *)g_slice_alloc0(sizeof(struct selection_data));
+    data->row = gtk_tree_row_reference_new(model, path);
+    data->set = server->set;
+    return data;
+}
+
+void selection_data_free(struct selection_data *data)
+{
+    gtk_tree_row_reference_free(data->row);
+    g_slice_free(struct selection_data, data);
+}
+
+void play_window_destroy(GtkWidget *window, gpointer user_data)
+{
+    struct selection_data *data = (struct selection_data *)user_data;
+    selection_data_free(data);
+}
+
+GtkWidget *play_window_image(struct selection_data *data)
+{
+    GError *error = NULL;
+    GdkPixbuf *icon = gtk_icon_theme_load_icon(gtk_icon_theme_get_default(),
+                                               "mediaplayer_default_album",
+                                               PLAY_WINDOW_IMAGE_SIZE,
+                                               0, &error);
+
+    data->art_image = gtk_image_new_from_pixbuf(icon);
+
+    return data->art_image;
+}
+
+void mp_callback(GtkWidget *event_box, GdkEventButton *event,
+                 gpointer user_data)
+{
+    struct toggle_data *data = (struct toggle_data *)user_data;
+    
+    if (event->type == GDK_BUTTON_PRESS) {
+        if (data->toggle) data->state ^= 1;
+        gtk_image_set_from_pixbuf(GTK_IMAGE(data->image),
+            data->pixbufs[data->toggle ? data->state : 1]);
+    } else {
+        if (!data->toggle) gtk_image_set_from_pixbuf(GTK_IMAGE(data->image),
+                                                     data->pixbufs[0]);
+        if (data->callback) (data->callback)(event_box, data->user_data);
+    }
+}
+
+GtkWidget *mp_button(char *file, char *onpress_file, gboolean toggle,
+                     void (*callback)(GtkWidget *, gpointer),
+                     gpointer user_data)
+{
+    char fname[256];
+    struct toggle_data *data = g_slice_alloc0(sizeof(struct toggle_data));
+    data->toggle = toggle;
+    data->user_data = user_data;
+    data->callback = callback;
+    
+    GError *error = NULL;
+    strcpy(fname, MP_ICON_PATH);
+    strcat(fname, file);
+    strcat(fname, ".png");
+    data->pixbufs[0] = gdk_pixbuf_new_from_file(fname, &error);
+
+    strcpy(fname, MP_ICON_PATH);
+    strcat(fname, onpress_file);
+    strcat(fname, ".png");
+    data->pixbufs[1] = gdk_pixbuf_new_from_file(fname, &error);
+    
+    data->image = GTK_IMAGE(gtk_image_new_from_pixbuf(data->pixbufs[0]));
+
+    GtkWidget *event_box = gtk_event_box_new();
+    gtk_container_add(GTK_CONTAINER(event_box), GTK_WIDGET(data->image));
+    
+    g_signal_connect(G_OBJECT(event_box), "button_press_event",
+                     G_CALLBACK(mp_callback), data);
+    g_signal_connect(G_OBJECT(event_box), "button_release_event",
+                     G_CALLBACK(mp_callback), data);
+    return event_box;
+}
+
+GtkWidget *play_window(struct proxy *server, struct selection_data *data)
 {
     GtkWidget *window = hildon_stackable_window_new();
     gtk_window_set_title(GTK_WINDOW(window), PLAY_WINDOW_TITLE);
@@ -568,10 +687,10 @@ GtkWidget *play_window(struct proxy *server, GtkTreeRowReference *row)
     gtk_container_add(GTK_CONTAINER(window), vbox);
     
     GtkWidget *hbox = gtk_hbox_new(FALSE, 0);
-    gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 6);
+    gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
     
-    GtkWidget *image = gtk_image_new();
-    gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 6);
+    GtkWidget *image = play_window_image(data);
+    gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 20);
     
     GtkWidget *inner_box = gtk_vbox_new(FALSE, 0);
     gtk_box_pack_start(GTK_BOX(hbox), inner_box, TRUE, TRUE, 6);
@@ -579,14 +698,24 @@ GtkWidget *play_window(struct proxy *server, GtkTreeRowReference *row)
     GtkWidget *button_box = gtk_hbutton_box_new();
     gtk_box_pack_start(GTK_BOX(vbox), button_box, FALSE, FALSE, 6);
     
-    GtkWidget *button =
-        hildon_button_new_with_text(HILDON_SIZE_FINGER_HEIGHT,
-                                    HILDON_BUTTON_ARRANGEMENT_VERTICAL,
-                                    "Play", "");
+    GtkWidget *back = mp_button("Back", "BackPressed", FALSE, NULL, NULL);
+    gtk_box_pack_start(GTK_BOX(button_box), back, FALSE, FALSE, 0);
     
-    g_signal_connect(button, "clicked", G_CALLBACK(play_button), server->set);
+    GtkWidget *play = mp_button("Play", "Pause", TRUE, play_button,
+                                data->set);
+    gtk_box_pack_start(GTK_BOX(button_box), play, FALSE, FALSE, 0);
     
-    gtk_box_pack_start(GTK_BOX(button_box), button, FALSE, FALSE, 0);
+    GtkWidget *forward = mp_button("Forward", "ForwardPressed", FALSE,
+                                   NULL, NULL);
+    gtk_box_pack_start(GTK_BOX(button_box), forward, FALSE, FALSE, 0);
+    
+    GtkWidget *shuffle = mp_button("Shuffle", "ShufflePressed", TRUE, NULL, NULL);
+    gtk_box_pack_start(GTK_BOX(button_box), shuffle, FALSE, FALSE, 0);
+    
+    GtkWidget *repeat = mp_button("Repeat", "RepeatPressed", TRUE, NULL, NULL);
+    gtk_box_pack_start(GTK_BOX(button_box), repeat, FALSE, FALSE, 0);
+    
+    g_signal_connect(window, "destroy", G_CALLBACK(play_window_destroy), data);
     gtk_widget_show_all(GTK_WIDGET(vbox));
     
     return window;
@@ -615,15 +744,15 @@ void content_select(HildonTouchSelector *selector, gint column, gpointer data)
     if (container) {
         GtkListStore *view_list;
         window = content_window(server, label, &view_list);
-        browse(content, id, 0, MAX_BROWSE, view_list, server->set->didl_parser);
+        browse(content, id, 0, MAX_BROWSE, view_list, server->set);
         gupnp_service_proxy_add_notify(content, "ContainerUpdateIDs",
                                        G_TYPE_STRING, on_container_update_ids,
                                        NULL);
         gupnp_service_proxy_set_subscribed(content, TRUE);
     } else {
-        GtkTreeRowReference *row = gtk_tree_row_reference_new(model, path);
-        transport_selection(server, row);
-        window = play_window(server, row);
+        struct selection_data *data = selection_data_new(server, model, path);
+        transport_selection(server, data);
+        window = play_window(server, data);
     }
     
     HildonWindowStack *stack = hildon_window_stack_get_default();
@@ -674,8 +803,7 @@ void server_select(HildonTouchSelector *selector, gint column, gpointer data)
     GtkListStore *view_list;
     GtkWidget *window = content_window(server, server->name, &view_list);
 
-    browse(content_dir, "0", 0, MAX_BROWSE, view_list,
-           server->set->didl_parser);
+    browse(content_dir, "0", 0, MAX_BROWSE, view_list, server->set);
 
     gupnp_service_proxy_add_notify(content_dir, "ContainerUpdateIDs",
                                    G_TYPE_STRING, on_container_update_ids,
@@ -721,7 +849,7 @@ GtkWidget *main_menu(struct proxy_set *proxy_set)
     GtkWidget *selector = target_selector(proxy_set);
     hildon_picker_button_set_selector(HILDON_PICKER_BUTTON(button),
                                       HILDON_TOUCH_SELECTOR(selector));
-    proxy_set->renderer_picker = HILDON_PICKER_BUTTON(button);
+    proxy_set->app->renderer_picker = HILDON_PICKER_BUTTON(button);
     
     hildon_app_menu_append(HILDON_APP_MENU(menu), GTK_BUTTON(button));
 
@@ -736,11 +864,6 @@ GtkWidget *main_window(HildonProgram *program, struct proxy_set *proxy_set)
 
     gtk_window_set_title(GTK_WINDOW(window), APPLICATION_NAME);
 
-    GError *error = NULL;
-    proxy_set->icon = gtk_icon_theme_load_icon(gtk_icon_theme_get_default(),
-                                               "control_bluetooth_lan",
-                                               HILDON_ICON_PIXEL_SIZE_FINGER,
-                                               0, &error);
     proxy_set->server_list = gtk_list_store_new(NUM_COLS, GDK_TYPE_PIXBUF,
                                                 G_TYPE_STRING, G_TYPE_STRING,
                                                 G_TYPE_POINTER, G_TYPE_BOOLEAN);
@@ -790,7 +913,7 @@ void add_renderer(GUPnPDeviceProxy *proxy, struct proxy_set *proxy_set)
     char *name = gupnp_device_info_get_friendly_name(GUPNP_DEVICE_INFO(proxy));
     if (!name) return;
     
-    server = (struct proxy *)g_malloc(sizeof(struct proxy));
+    server = (struct proxy *)g_slice_alloc(sizeof(struct proxy));
     server->proxy = proxy;
     server->name = name;
     server->set = proxy_set;
@@ -801,7 +924,7 @@ void add_renderer(GUPnPDeviceProxy *proxy, struct proxy_set *proxy_set)
         0, server->name, 1, udn, -1);
     
     /* TODO: default to saved value */
-    hildon_picker_button_set_active(proxy_set->renderer_picker, 0);
+    hildon_picker_button_set_active(proxy_set->app->renderer_picker, 0);
 
     GtkTreeModel *model = GTK_TREE_MODEL(proxy_set->renderer_list);
     GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
@@ -830,16 +953,21 @@ void add_server(GUPnPDeviceProxy *proxy, struct proxy_set *proxy_set)
     
     if (!name || !content_dir) return;
 
-    server = (struct proxy *)g_malloc(sizeof(struct proxy));
+    server = (struct proxy *)g_slice_alloc(sizeof(struct proxy));
     server->proxy = proxy;
     server->name = name;
     server->set = proxy_set;
     
+    GError *error = NULL;
+    GdkPixbuf *icon = gtk_icon_theme_load_icon(gtk_icon_theme_get_default(),
+                                               "control_bluetooth_lan",
+                                               HILDON_ICON_PIXEL_SIZE_FINGER,
+                                               0, &error);
     GtkTreeIter iter;
     gtk_list_store_append(proxy_set->server_list, &iter);
     gtk_list_store_set(proxy_set->server_list, &iter,
-        COL_ICON, proxy_set->icon, COL_LABEL, server->name, COL_ID, udn,
-        COL_CONTENT, NULL, COL_CONTAINER, TRUE, -1);
+                       COL_ICON, icon, COL_LABEL, server->name, COL_ID, udn,
+                       COL_CONTENT, NULL, COL_CONTAINER, TRUE, -1);
 
     GtkTreeModel *model = GTK_TREE_MODEL(proxy_set->server_list);
     GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
@@ -933,6 +1061,23 @@ void init_upnp(struct proxy_set *proxy_set)
     gssdp_resource_browser_set_active(GSSDP_RESOURCE_BROWSER(cp), TRUE);
 }
 
+void init_application(struct application *app)
+{
+    struct proxy_set *set = &app->set;
+    set->renderers = g_hash_table_new(g_str_hash, g_str_equal);
+    set->servers = g_hash_table_new(g_str_hash, g_str_equal);
+
+    set->renderer_list = gtk_list_store_new(2, G_TYPE_STRING,
+                                               G_TYPE_STRING);
+    set->server_list = gtk_list_store_new(NUM_COLS, GDK_TYPE_PIXBUF,
+                                          G_TYPE_STRING, G_TYPE_STRING,
+                                          G_TYPE_POINTER, G_TYPE_BOOLEAN);
+    set->app = app;
+
+    app->didl_parser = gupnp_didl_lite_parser_new();
+    app->soup = soup_session_async_new();
+}
+
 int main(int argc, char *argv[])
 {
     g_thread_init(NULL);
@@ -942,18 +1087,14 @@ int main(int argc, char *argv[])
     HildonProgram *program = hildon_program_get_instance();
     g_set_application_name(APPLICATION_NAME);
 
-    struct proxy_set proxy_set;
-    proxy_set.renderers = g_hash_table_new(g_str_hash, g_str_equal);
-    proxy_set.servers = g_hash_table_new(g_str_hash, g_str_equal);
+    struct application app;
+    init_application(&app);
 
-    proxy_set.renderer_list = gtk_list_store_new(2, G_TYPE_STRING,
-                                                    G_TYPE_STRING);
-    proxy_set.didl_parser = gupnp_didl_lite_parser_new();
-    GtkWidget *window = main_window(program, &proxy_set);
+    GtkWidget *window = main_window(program, &app.set);
     g_signal_connect(G_OBJECT(window), "destroy",
                      G_CALLBACK(gtk_main_quit), NULL);
     
-    init_upnp(&proxy_set);
+    init_upnp(&app.set);
 
     gtk_widget_show(window);
     gtk_main();