[easytag/wip/musicbrainz-dialog] Add simple asynchronous query support



commit 52afca8e0bc167f6a4fa52f6c07c77c3ac6cab7c
Author: David King <amigadave amigadave com>
Date:   Tue Nov 25 00:17:32 2014 +0000

    Add simple asynchronous query support
    
    Use GSimpleAsyncResult to dispatch MusicBrainz queries from a thread, in
    order to keep the UI responsive.

 data/musicbrainz_dialog.ui |   17 +++
 src/musicbrainz.c          |  321 ++++++++++++++++++++++++++++++++++++++++++++
 src/musicbrainz.h          |   29 ++++-
 src/musicbrainz_dialog.c   |  158 ++++++++++++++++++++++
 4 files changed, 524 insertions(+), 1 deletions(-)
---
diff --git a/data/musicbrainz_dialog.ui b/data/musicbrainz_dialog.ui
index f029b5a..14c02d2 100644
--- a/data/musicbrainz_dialog.ui
+++ b/data/musicbrainz_dialog.ui
@@ -1,5 +1,10 @@
 <interface domain="easytag">
     <requires lib="gtk+" version="3.4"/>
+    <object class="GtkListStore" id="results_model">
+        <columns>
+            <column type="gchararray"/>
+        </columns>
+    </object>
     <object class="GtkGrid" id="musicbrainz_grid">
         <property name="column-spacing">6</property>
         <property name="row-spacing">6</property>
@@ -96,7 +101,19 @@
                 <property name="visible">True</property>
                 <child>
                     <object class="GtkTreeView" id="results_view">
+                        <property name="model">results_model</property>
                         <property name="visible">True</property>
+                        <child>
+                            <object class="GtkTreeViewColumn" id="name_column">
+                                <property name="title" translatable="yes">Name</property>
+                                <child>
+                                    <object class="GtkCellRendererText" id="name_renderer"/>
+                                    <attributes>
+                                        <attribute name="text">0</attribute>
+                                    </attributes>
+                                </child>
+                            </object>
+                        </child>
                     </object>
                 </child>
             </object>
diff --git a/src/musicbrainz.c b/src/musicbrainz.c
index 133c841..254d949 100644
--- a/src/musicbrainz.c
+++ b/src/musicbrainz.c
@@ -30,11 +30,261 @@ G_DEFINE_TYPE (EtMusicbrainz, et_musicbrainz, G_TYPE_OBJECT)
 
 #define et_musicbrainz_get_instance_private(musicbrainz) (musicbrainz->priv)
 
+#define GET(field, function, obj) \
+{ \
+    function (obj, buffer, sizeof (buffer)); \
+ \
+    if (field) \
+    { \
+        g_free (field); \
+    } \
+ \
+    if (*buffer == '\0') \
+    { \
+        field = NULL; \
+    } \
+    else \
+    { \
+        field = g_strdup (buffer); \
+    } \
+}
+
 struct _EtMusicbrainzPrivate
 {
     Mb5Query query;
 };
 
+struct _EtMusicbrainzQuery
+{
+    /*< private >*/
+    EtMusicbrainzEntity entity;
+    gchar *search_string;
+    gint ref_count;
+};
+
+struct _EtMusicbrainzResult
+{
+    /*< private >*/
+    GList *list;
+    gint ref_count;
+};
+
+EtMusicbrainzQuery *
+et_musicbrainz_query_new (EtMusicbrainzEntity entity,
+                          const gchar *search_string)
+{
+    EtMusicbrainzQuery *query;
+
+    query = g_slice_new (EtMusicbrainzQuery);
+    query->entity = entity;
+    query->ref_count = 1;
+    query->search_string = g_strdup (search_string);
+
+    return query;
+}
+
+EtMusicbrainzQuery *
+et_musicbrainz_query_ref (EtMusicbrainzQuery *query)
+{
+    g_return_val_if_fail (query != NULL, NULL);
+
+    g_atomic_int_inc (&query->ref_count);
+
+    return query;
+}
+
+void
+et_musicbrainz_query_unref (EtMusicbrainzQuery *query)
+{
+    g_return_if_fail (query != NULL);
+
+    if (g_atomic_int_dec_and_test (&query->ref_count))
+    {
+        g_free (query->search_string);
+        g_slice_free (EtMusicbrainzQuery, query);
+    }
+}
+
+static EtMusicbrainzResult *
+et_musicbrainz_result_new (EtMusicbrainzEntity entity,
+                           Mb5Metadata metadata)
+{
+    EtMusicbrainzResult *result;
+    GList *list = NULL;
+    gchar buffer[512]; /* For the GET macro. */
+
+    result = g_slice_new (EtMusicbrainzResult);
+    result->ref_count = 1;
+
+    switch (entity)
+    {
+        case ET_MUSICBRAINZ_ENTITY_ARTIST:
+        {
+            Mb5ArtistList artist_list;
+            gsize i;
+            gint n_artists;
+
+            artist_list = mb5_metadata_get_artistlist (metadata);
+            n_artists = mb5_artist_list_size (artist_list);
+
+            for (i = 0; i < n_artists; i++)
+            {
+                Mb5Artist artist;
+                gchar *name = NULL;
+
+                /* TODO: Come up with a full object for describing an
+                 * artist. */
+                artist = mb5_artist_list_item (artist_list, i);
+                GET (name, mb5_artist_get_name, artist);
+                list = g_list_prepend (list, name);
+            }
+        }
+            break;
+        case ET_MUSICBRAINZ_ENTITY_RELEASE_GROUP:
+        {
+            Mb5ReleaseGroupList release_group_list;
+            gsize i;
+            gint n_release_groups;
+
+            release_group_list = mb5_metadata_get_releasegrouplist (metadata);
+            n_release_groups = mb5_releasegroup_list_size (release_group_list);
+
+            for (i = 0; i < n_release_groups; i++)
+            {
+                Mb5ReleaseGroup release_group;
+                gchar *name = NULL;
+
+                /* TODO: Come up with a full object for describing a release
+                 * group. */
+                release_group = mb5_releasegroup_list_item (release_group_list,
+                                                            i);
+                GET (name, mb5_releasegroup_get_title, release_group);
+                list = g_list_prepend (list, name);
+            }
+        }
+            break;
+        default:
+            g_assert_not_reached ();
+            break;
+    }
+
+    result->list = g_list_reverse (list);
+
+    return result;
+}
+
+EtMusicbrainzResult *
+et_musicbrainz_result_ref (EtMusicbrainzResult *result)
+{
+    g_return_val_if_fail (result != NULL, NULL);
+
+    g_atomic_int_inc (&result->ref_count);
+
+    return result;
+}
+
+void
+et_musicbrainz_result_unref (EtMusicbrainzResult *result)
+{
+    g_return_if_fail (result != NULL);
+
+    if (g_atomic_int_dec_and_test (&result->ref_count))
+    {
+        g_list_free_full (result->list, g_free);
+        g_slice_free (EtMusicbrainzResult, result);
+    }
+}
+
+GList *
+et_musicbrainz_result_get_results (EtMusicbrainzResult *result)
+{
+    g_return_val_if_fail (result != NULL, NULL);
+
+    return result->list;
+}
+
+static EtMusicbrainzResult *
+query_mb_from_et_query (Mb5Query mb_query,
+                        EtMusicbrainzQuery *query)
+{
+    const gchar *entity;
+    gchar *param_names[1] = { "query" };
+    gchar *param_values[1];
+    Mb5Metadata metadata;
+    EtMusicbrainzResult *result = NULL;
+
+    switch (query->entity)
+    {
+        case ET_MUSICBRAINZ_ENTITY_ARTIST:
+            entity = "artist";
+            param_values[0] = g_strconcat ("artist:",
+                                           query->search_string, NULL);
+            break;
+        case ET_MUSICBRAINZ_ENTITY_RELEASE_GROUP:
+            entity = "release-group";
+            param_values[0] = g_strconcat ("releasegroup:",
+                                           query->search_string, NULL);
+            break;
+        default:
+            g_assert_not_reached ();
+            break;
+    }
+
+    metadata = mb5_query_query (mb_query, entity, "", "", 1, param_names,
+                                param_values);
+
+    g_free (param_values[0]);
+
+    if (metadata != NULL)
+    {
+        result = et_musicbrainz_result_new (query->entity, metadata);
+        mb5_metadata_delete (metadata);
+    }
+
+    return result;
+}
+
+static void
+search_thread_func (GSimpleAsyncResult *result,
+                    GObject *musicbrainz,
+                    GCancellable *cancellable)
+{
+    EtMusicbrainz *self;
+    EtMusicbrainzPrivate *priv;
+    EtMusicbrainzQuery *query;
+    EtMusicbrainzResult *mb_result;
+
+    self = ET_MUSICBRAINZ (musicbrainz);
+    priv = et_musicbrainz_get_instance_private (self);
+
+    query = g_simple_async_result_get_op_res_gpointer (result);
+
+    mb_result = query_mb_from_et_query (priv->query, query);
+
+    if (mb_result == NULL)
+    {
+        gint code;
+        gint len;
+        gchar *message;
+
+        code = mb5_query_get_lasthttpcode (priv->query);
+        len = mb5_query_get_lasterrormessage (priv->query, NULL, 0);
+        message = g_malloc (len);
+        mb5_query_get_lasterrormessage (priv->query, message, len);
+
+        /* TODO: Use a translatable string? */
+        g_simple_async_result_set_error (result, G_IO_ERROR, G_IO_ERROR_FAILED,
+                                         "MusicBrainz search query failed with error code '%d': %s",
+                                         code, message);
+        g_free (message);
+    }
+    else
+    {
+        g_simple_async_result_set_op_res_gpointer (result, mb_result,
+                                                   (GDestroyNotify)et_musicbrainz_result_unref);
+    }
+}
+
 static void
 et_musicbrainz_finalize (GObject *object)
 {
@@ -90,4 +340,75 @@ et_musicbrainz_new (void)
     return g_object_new (ET_TYPE_MUSICBRAINZ, NULL);
 }
 
+/*
+ * et_musicbrainz_search_async:
+ * @self: the musicbrainz context from which to initiate the search
+ * @query: parameters for the search query
+ * @cancellable: a cancellable, or %NULL
+ * @callback: a callback to trigger when the call is complete
+ * @user_data: user data to be passed to @callback
+ *
+ * Start an asynchronous search, submitting a query according to @info and
+ * delivering the result to @callback when complete.
+ */
+void
+et_musicbrainz_search_async (EtMusicbrainz *self,
+                             EtMusicbrainzQuery *query,
+                             GCancellable *cancellable,
+                             GAsyncReadyCallback callback,
+                             gpointer user_data)
+{
+    GSimpleAsyncResult *result;
+
+    g_return_if_fail (ET_MUSICBRAINZ (self));
+    g_return_if_fail (query != NULL && callback != NULL);
+
+    result = g_simple_async_result_new (G_OBJECT (self), callback, user_data,
+                                        et_musicbrainz_search_async);
+
+    g_simple_async_result_set_op_res_gpointer (result,
+                                               et_musicbrainz_query_ref (query),
+                                               (GDestroyNotify)et_musicbrainz_query_unref);
+    g_simple_async_result_run_in_thread (result, search_thread_func,
+                                         G_PRIORITY_DEFAULT, cancellable);
+    g_object_unref (result);
+}
+
+/*
+ * et_musicbrainz_search_finish:
+ * @self: the musicbrainz context on which the search was initiated
+ * @result: the result obtained in the ready callback from
+ * et_musicbrainz_search_async()
+ * @error: an error, or %NULL
+ *
+ * Finishes an asynchronous operation started with
+ * et_musicbrainz_search_async().
+ *
+ * Returns: a result, or %NULL on error
+ */
+EtMusicbrainzResult *
+et_musicbrainz_search_finish (EtMusicbrainz *self,
+                              GAsyncResult *result,
+                              GError **error)
+{
+    GSimpleAsyncResult *simple;
+    EtMusicbrainzResult *mb_result;
+
+    g_return_val_if_fail (g_simple_async_result_is_valid (result,
+                                                          G_OBJECT (self),
+                                                          et_musicbrainz_search_async),
+                          NULL);
+
+    simple = (GSimpleAsyncResult *)result;
+
+    if (g_simple_async_result_propagate_error (simple, error))
+    {
+        return NULL;
+    }
+
+    mb_result = (EtMusicbrainzResult *)g_simple_async_result_get_op_res_gpointer (simple);
+
+    return et_musicbrainz_result_ref (mb_result);
+}
+
 #endif /* ENABLE_MUSICBRAINZ */
diff --git a/src/musicbrainz.h b/src/musicbrainz.h
index 4cab216..897e6d6 100644
--- a/src/musicbrainz.h
+++ b/src/musicbrainz.h
@@ -23,7 +23,7 @@
 
 #ifdef ENABLE_MUSICBRAINZ
 
-#include <glib-object.h>
+#include <gio/gio.h>
 
 G_BEGIN_DECLS
 
@@ -34,6 +34,22 @@ typedef struct _EtMusicbrainz EtMusicbrainz;
 typedef struct _EtMusicbrainzClass EtMusicbrainzClass;
 typedef struct _EtMusicbrainzPrivate EtMusicbrainzPrivate;
 
+/*
+ * EtMusicbrainzEntity:
+ * @ET_MUSICBRAINZ_ENTITY_RELEASE_GROUP:
+ * @ET_MUSICBRAINZ_ENTITY_ARTIST:
+ *
+ * Entity type to query against the MusicBrainz web service.
+ */
+typedef enum
+{
+    ET_MUSICBRAINZ_ENTITY_RELEASE_GROUP,
+    ET_MUSICBRAINZ_ENTITY_ARTIST
+} EtMusicbrainzEntity;
+
+typedef struct _EtMusicbrainzQuery EtMusicbrainzQuery;
+typedef struct _EtMusicbrainzResult EtMusicbrainzResult;
+
 struct _EtMusicbrainz
 {
     /*< private >*/
@@ -50,6 +66,17 @@ struct _EtMusicbrainzClass
 GType et_musicbrainz_get_type (void);
 EtMusicbrainz *et_musicbrainz_new (void);
 
+void et_musicbrainz_search_async (EtMusicbrainz *self, EtMusicbrainzQuery *query, GCancellable *cancellable, 
GAsyncReadyCallback callback, gpointer user_data);
+EtMusicbrainzResult * et_musicbrainz_search_finish (EtMusicbrainz *self, GAsyncResult *result, GError 
**error);
+
+EtMusicbrainzQuery * et_musicbrainz_query_new (EtMusicbrainzEntity entity, const gchar *search_string);
+EtMusicbrainzQuery * et_musicbrainz_query_ref (EtMusicbrainzQuery *query);
+void et_musicbrainz_query_unref (EtMusicbrainzQuery *query);
+
+EtMusicbrainzResult * et_musicbrainz_result_ref (EtMusicbrainzResult *result);
+void et_musicbrainz_result_unref (EtMusicbrainzResult *result);
+GList * et_musicbrainz_result_get_results (EtMusicbrainzResult *result);
+
 G_END_DECLS
 
 #endif /* ENABLE_MUSICBRAINZ */
diff --git a/src/musicbrainz_dialog.c b/src/musicbrainz_dialog.c
index 5fe7144..166c478 100644
--- a/src/musicbrainz_dialog.c
+++ b/src/musicbrainz_dialog.c
@@ -37,9 +37,138 @@ static guint BOX_SPACING = 6;
 struct _EtMusicbrainzDialogPrivate
 {
     EtMusicbrainz *mb;
+
+    GtkWidget *search_combo;
+    GtkWidget *search_entry;
+    GtkWidget *search_button;
+    GtkWidget *stop_button;
+    GtkWidget *results_view;
+
+    GtkListStore *results_model;
+
+    GCancellable *cancellable;
 };
 
 static void
+stop_search (EtMusicbrainzDialog *self)
+{
+    EtMusicbrainzDialogPrivate *priv;
+
+    priv = et_musicbrainz_dialog_get_instance_private (self);
+
+    gtk_widget_set_sensitive (priv->search_combo, TRUE);
+    gtk_widget_set_sensitive (priv->search_entry, TRUE);
+    gtk_widget_set_sensitive (priv->search_button, TRUE);
+    gtk_widget_set_sensitive (priv->stop_button, FALSE);
+    gtk_widget_set_sensitive (priv->results_view, TRUE);
+}
+
+static void
+on_stop_button_clicked (EtMusicbrainzDialog *self,
+                        GtkButton *stop_button)
+{
+    EtMusicbrainzDialogPrivate *priv;
+
+    priv = et_musicbrainz_dialog_get_instance_private (self);
+
+    g_cancellable_cancel (priv->cancellable);
+    g_object_unref (priv->cancellable);
+    priv->cancellable = g_cancellable_new ();
+
+    stop_search (self);
+}
+
+static void
+add_string_to_results_model (const gchar *string,
+                             GtkListStore *model)
+{
+    gtk_list_store_insert_with_values (model, NULL, -1, 0, string, -1);
+}
+
+static void
+query_complete_cb (GObject *source_object,
+                   GAsyncResult *res,
+                   gpointer user_data)
+{
+    EtMusicbrainzDialog *self;
+    EtMusicbrainzDialogPrivate *priv;
+    EtMusicbrainzResult *result;
+    GError *error = NULL;
+    GList *results;
+
+    self = ET_MUSICBRAINZ_DIALOG (user_data);
+    priv = et_musicbrainz_dialog_get_instance_private (self);
+
+    result = et_musicbrainz_search_finish (priv->mb, res, &error);
+
+    if (!result)
+    {
+        if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        {
+            g_debug ("%s", "MusicBrainz search cancelled by user");
+        }
+        else
+        {
+            /* TODO: Show the error in the UI. */
+            g_message ("Search failed: %s", error->message);
+        }
+
+        g_error_free (error);
+        return;
+    }
+
+    results = et_musicbrainz_result_get_results (result);
+
+    g_list_foreach (results, (GFunc)add_string_to_results_model,
+                    priv->results_model);
+
+    et_musicbrainz_result_unref (result);
+
+    stop_search (self);
+}
+
+static void
+start_search (EtMusicbrainzDialog *self)
+{
+    EtMusicbrainzDialogPrivate *priv;
+    EtMusicbrainzEntity entity;
+    EtMusicbrainzQuery *query;
+
+    priv = et_musicbrainz_dialog_get_instance_private (self);
+
+    gtk_widget_set_sensitive (priv->search_combo, FALSE);
+    gtk_widget_set_sensitive (priv->search_entry, FALSE);
+    gtk_widget_set_sensitive (priv->search_button, FALSE);
+    gtk_widget_set_sensitive (priv->stop_button, TRUE);
+    gtk_widget_set_sensitive (priv->results_view, FALSE);
+
+    gtk_list_store_clear (priv->results_model);
+
+    entity = gtk_combo_box_get_active (GTK_COMBO_BOX (priv->search_combo));
+    query = et_musicbrainz_query_new (entity,
+                                      gtk_entry_get_text (GTK_ENTRY (priv->search_entry)));
+
+    et_musicbrainz_search_async (priv->mb, query, priv->cancellable,
+                                 query_complete_cb, self);
+
+    et_musicbrainz_query_unref (query);
+}
+
+static void
+on_search_button_clicked (EtMusicbrainzDialog *self,
+                          GtkButton *search_button)
+{
+    start_search (self);
+}
+
+static void
+on_search_entry_activate (EtMusicbrainzDialog *self,
+                          GtkEntry *search_entry)
+{
+    start_search (self);
+}
+
+static void
 create_musicbrainz_dialog (EtMusicbrainzDialog *self)
 {
     EtMusicbrainzDialogPrivate *priv;
@@ -71,6 +200,28 @@ create_musicbrainz_dialog (EtMusicbrainzDialog *self)
                  error->message);
     }
 
+    priv->search_combo = GTK_WIDGET (gtk_builder_get_object (builder,
+                                                             "search_combo"));
+    priv->search_entry = GTK_WIDGET (gtk_builder_get_object (builder,
+                                                             "search_entry"));
+    g_signal_connect_swapped (priv->search_entry, "activate",
+                              G_CALLBACK (on_search_entry_activate), self);
+
+    priv->search_button = GTK_WIDGET (gtk_builder_get_object (builder,
+                                                              "search_button"));
+    g_signal_connect_swapped (priv->search_button, "clicked",
+                              G_CALLBACK (on_search_button_clicked), self);
+
+    priv->stop_button = GTK_WIDGET (gtk_builder_get_object (builder,
+                                                            "stop_button"));
+    g_signal_connect_swapped (priv->stop_button, "clicked",
+                              G_CALLBACK (on_stop_button_clicked), self);
+
+    priv->results_model = GTK_LIST_STORE (gtk_builder_get_object (builder,
+                                                                  "results_model"));
+    priv->results_view = GTK_WIDGET (gtk_builder_get_object (builder,
+                                                             "results_view"));
+
     grid = GTK_WIDGET (gtk_builder_get_object (builder, "musicbrainz_grid"));
     gtk_container_add (GTK_CONTAINER (content_area), grid);
 
@@ -88,6 +239,12 @@ et_musicbrainz_dialog_finalize (GObject *object)
 
     g_clear_object (&priv->mb);
 
+    if (priv->cancellable)
+    {
+        g_cancellable_cancel (priv->cancellable);
+        g_clear_object (&priv->cancellable);
+    }
+
     G_OBJECT_CLASS (et_musicbrainz_dialog_parent_class)->finalize (object);
 }
 
@@ -101,6 +258,7 @@ et_musicbrainz_dialog_init (EtMusicbrainzDialog *self)
                                                      EtMusicbrainzDialogPrivate);
 
     priv->mb = et_musicbrainz_new ();
+    priv->cancellable = g_cancellable_new ();
 
     create_musicbrainz_dialog (self);
 }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]