[grilo-plugins] pocket: Add Pocket source



commit ec026789c4a72634ab86f56d87845d01be4fe7a1
Author: Bastien Nocera <hadess hadess net>
Date:   Sun Feb 2 18:15:02 2014 +0100

    pocket: Add Pocket source
    
    A number of improvements can still be done:
    - Respect browse spec (count, skip)
    - Add change support
    - Find a way to get thumbnails
    - Add delete/archive support
    
    https://bugzilla.gnome.org/show_bug.cgi?id=722819

 configure.ac                    |   53 +++
 src/Makefile.am                 |    6 +-
 src/pocket/Makefile.am          |   52 +++
 src/pocket/channel-pocket.svg   |  137 ++++++
 src/pocket/gnome-pocket.c       |  934 +++++++++++++++++++++++++++++++++++++++
 src/pocket/gnome-pocket.h       |  117 +++++
 src/pocket/grl-pocket.c         |  405 +++++++++++++++++
 src/pocket/grl-pocket.h         |   76 ++++
 src/pocket/grl-pocket.xml       |   10 +
 src/pocket/pocket.gresource.xml |    6 +
 10 files changed, 1795 insertions(+), 1 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index d3dae9f..b9f08ac 100644
--- a/configure.ac
+++ b/configure.ac
@@ -146,6 +146,8 @@ PKG_CHECK_MODULES(GOA, [goa-1.0 >= 3.7.1], HAVE_GOA=yes, HAVE_GOA=no)
 
 PKG_CHECK_MODULES(TOTEM_PL_PARSER, totem-plparser >= 3.4.1, HAVE_TOTEM_PL_PARSER=yes, 
HAVE_TOTEM_PL_PARSER=no)
 
+PKG_CHECK_MODULES(REST, rest-0.7, HAVE_REST=yes, HAVE_REST=no)
+
 PKG_CHECK_MODULES([JSON], [json-glib-1.0], HAVE_JSON_GLIB=yes, HAVE_JSON_GLIB=no)
 
 PKG_CHECK_MODULES(GMIME, gmime-2.6,
@@ -545,6 +547,56 @@ fi
 AC_SUBST(DEPS_FLICKR_LIBS)
 
 # ----------------------------------------------------------
+# BUILD POCKET PLUGIN
+# ----------------------------------------------------------
+
+AC_ARG_ENABLE(pocket,
+        AC_HELP_STRING([--enable-pocket],
+                [enable Pocket plugin (default: auto)]),
+        [
+                case "$enableval" in
+                     yes)
+                        if test "x$HAVE_JSON_GLIB" = "xno"; then
+                           AC_MSG_ERROR([json-glib not found, install it or use --disable-pocket])
+                        fi
+                        if test "x$HAVE_GOA" = "xno"; then
+                           AC_MSG_ERROR([gnome-online-accounts not found, install it or use 
--disable-pocket])
+                        fi
+                        if test "x$HAVE_REST" = "xno"; then
+                           AC_MSG_ERROR([rest not found, install it or use --disable-pocket])
+                        fi
+                        if test "x$HAVE_TOTEM_PL_PARSER" = "xno"; then
+                           AC_MSG_ERROR([totem-pl-parser not found, install it or use --disable-pocket])
+                        fi
+                        ;;
+                esac
+        ],
+        [
+                if test "x$HAVE_JSON_GLIB" = "xyes" -a "x$HAVE_GOA" = "xyes" -a "x$HAVE_REST" = "xyes" -a 
"x$HAVE_TOTEM_PL_PARSER" = "xyes"; then
+                   enable_pocket=yes
+                else
+                   enable_pocket=no
+                fi
+        ])
+
+AM_CONDITIONAL([POCKET_PLUGIN], [test "x$enable_pocket" = "xyes"])
+GRL_PLUGINS_ALL="$GRL_PLUGINS_ALL pocket"
+if test "x$enable_pocket" = "xyes"
+then
+       GRL_PLUGINS_ENABLED="$GRL_PLUGINS_ENABLED pocket"
+fi
+
+POCKET_PLUGIN_ID="grl-pocket"
+AC_SUBST(POCKET_PLUGIN_ID)
+AC_DEFINE_UNQUOTED([POCKET_PLUGIN_ID], ["$POCKET_PLUGIN_ID"], [Pocket plugin ID])
+
+DEPS_POCKET_CFLAGS="$DEPS_CFLAGS $JSON_CFLAGS $GOA_CFLAGS $REST_CFLAGS $TOTEM_PL_PARSER_CFLAGS"
+AC_SUBST(DEPS_POCKET_CFLAGS)
+
+DEPS_POCKET_LIBS="$DEPS_LIBS $JSON_LIBS $GOA_LIBS $REST_LIBS $TOTEM_PL_PARSER_LIBS"
+AC_SUBST(DEPS_POCKET_LIBS)
+
+# ----------------------------------------------------------
 # BUILD PODCASTS PLUGIN
 # ----------------------------------------------------------
 
@@ -1264,6 +1316,7 @@ AC_CONFIG_FILES([
   src/magnatune/Makefile
   src/metadata-store/Makefile
   src/optical-media/Makefile
+  src/pocket/Makefile
   src/podcasts/Makefile
   src/raitv/Makefile
   src/shoutcast/Makefile
diff --git a/src/Makefile.am b/src/Makefile.am
index 4fc5a9d..467b6fd 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -62,6 +62,10 @@ if OPTICAL_MEDIA_PLUGIN
 SUBDIRS += optical-media
 endif
 
+if POCKET_PLUGIN
+SUBDIRS += pocket
+endif
+
 if PODCASTS_PLUGIN
 SUBDIRS += podcasts
 endif
@@ -105,7 +109,7 @@ endif
 DIST_SUBDIRS = \
    apple-trailers bliptv bookmarks dmap filesystem flickr freebox gravatar guardian-videos jamendo \
    lastfm-albumart local-metadata magnatune metadata-store optical-media   \
-   podcasts raitv shoutcast tmdb tracker upnp vimeo youtube
+   pocket podcasts raitv shoutcast tmdb tracker upnp vimeo youtube
 
 MAINTAINERCLEANFILES = \
         *.in           \
diff --git a/src/pocket/Makefile.am b/src/pocket/Makefile.am
new file mode 100644
index 0000000..38590ea
--- /dev/null
+++ b/src/pocket/Makefile.am
@@ -0,0 +1,52 @@
+#
+# Makefile.am
+#
+# Author: Bastien Nocera <hadess hadess net>
+#
+# Copyright (C) 2013 Bastien Nocera
+
+include $(top_srcdir)/gtester.mk
+
+ext_LTLIBRARIES                 = libgrlpocket.la
+
+libgrlpocket_la_CFLAGS =                       \
+       $(DEPS_POCKET_CFLAGS)   \
+       -DG_LOG_DOMAIN=\"GrlPocket\"    \
+       -DLOCALEDIR=\"$(localedir)\"
+
+libgrlpocket_la_LIBADD =       \
+       $(DEPS_POCKET_LIBS)
+
+libgrlpocket_la_LDFLAGS = \
+       -no-undefined         \
+       -module               \
+       -avoid-version
+
+libgrlpocket_la_SOURCES =      \
+       grl-pocket.c            \
+       grl-pocket.h            \
+       gnome-pocket.h          \
+       gnome-pocket.c          \
+       pocketresources.h       \
+       pocketresources.c
+
+pocketresources.h: pocket.gresource.xml
+       $(AM_V_GEN) $(GLIB_COMPILE_RESOURCES) $(srcdir)/pocket.gresource.xml \
+               --target=$@ --sourcedir=$(srcdir) --c-name _grl_pocket --generate-header
+pocketresources.c: pocket.gresource.xml pocketresources.h channel-pocket.svg
+       $(AM_V_GEN) $(GLIB_COMPILE_RESOURCES) $(srcdir)/pocket.gresource.xml \
+               --target=$@ --sourcedir=$(srcdir) --c-name _grl_pocket --generate-source
+
+extdir         = $(GRL_PLUGINS_DIR)
+pocketxmldir   = $(GRL_PLUGINS_DIR)
+pocketxml_DATA = $(POCKET_PLUGIN_ID).xml
+
+EXTRA_DIST = $(pocketxml_DATA) channel-pocket.svg pocket.gresource.xml
+
+MAINTAINERCLEANFILES = \
+       *.in            \
+       *~
+
+CLEANFILES = pocketresources.h pocketresources.c
+
+DISTCLEANFILES = $(MAINTAINERCLEANFILES)
diff --git a/src/pocket/channel-pocket.svg b/src/pocket/channel-pocket.svg
new file mode 100644
index 0000000..90e834c
--- /dev/null
+++ b/src/pocket/channel-pocket.svg
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="256"
+   height="256"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="channel-youtube.svg">
+  <defs
+     id="defs4">
+    <clipPath
+       id="clipPath6193"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path6195"
+         d="m 1600,2252.8 5020,0 0,3650 -5020,0 0,-3650 z" />
+    </clipPath>
+    <linearGradient
+       id="linearGradient6181"
+       spreadMethod="pad"
+       gradientTransform="matrix(-3.593e-5,822,822,3.593e-5,411,0)"
+       gradientUnits="userSpaceOnUse"
+       y2="0"
+       x2="1"
+       y1="0"
+       x1="0">
+      <stop
+         id="stop6183"
+         offset="0"
+         style="stop-opacity:1;stop-color:#c01e25" />
+      <stop
+         id="stop6185"
+         offset="1"
+         style="stop-opacity:1;stop-color:#e62426" />
+    </linearGradient>
+    <clipPath
+       id="clipPath6177"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path6179"
+         d="M 8220,0 0,0 l 0,8220 8220,0 0,-8220 m -6620,5902.8 0,-3650 5020,0 0,3650 -5020,0" />
+    </clipPath>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#505050"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="1"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="120.49386"
+     inkscape:cy="175.09957"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     borderlayer="true"
+     inkscape:showpageshadow="false"
+     inkscape:window-width="2560"
+     inkscape:window-height="1374"
+     inkscape:window-x="0"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:snap-bbox="true"
+     inkscape:object-nodes="true" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-796.36218)">
+    <g
+       transform="matrix(630.025,0,0,-458.9,-77.5125,1218.8747)"
+       id="g6197" />
+    <path
+       
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+       d="m 30.115602,817.66358 197.356268,0 8.00002,11.43214 L 235,1033.6169 l -212.5,0 
-0.384417,-204.52118 z"
+       id="rect7653"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccccc" />
+    <path
+       
style="color:#000000;fill:#ee4055;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+       d="m 70.934854,870.79411 c -8.818633,0 -15.925195,7.19797 -15.925195,16.13479 l 0,34.39697 c 
0,46.31516 24.636814,78.60443 72.990311,78.60443 47.87064,0 72.99037,-32.01415 72.99037,-78.60443 l 
0,-34.39697 c 0,-8.93682 -7.1065,-16.13479 -15.92514,-16.13479 z m 90.664866,36.33346 c 2.50474,0 
5.01393,0.98537 6.937,2.93364 3.84636,3.89713 3.84636,10.22089 0,14.11802 l -32.84559,33.2774 c 
-2.11228,2.13974 -4.94066,4.42546 -7.6911,4.21491 -2.7505,0.21336 -5.57882,-2.07517 -7.69116,-4.21491 l 
-32.845583,-33.2774 c -3.846309,-3.89713 -3.846309,-10.22089 0,-14.11802 1.923126,-1.94827 4.457406,-2.90276 
6.967205,-2.90276 2.50986,0 4.98384,0.95449 6.906958,2.90276 l 26.66258,27.01316 26.66252,-27.01316 c 
1.92324,-1.94827 4.43236,-2.93364 6.93717,-2.93364 z"
+       id="path4636"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="sssssssssssscssssscss" />
+    <g
+       transform="matrix(5.6146396,0,0,5.6146396,-2740.2871,170.98003)"
+       style="display:inline;enable-background:new"
+       id="g10051">
+      <path
+         
style="color:#000000;fill:#75d4a5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new"
+         d="m 493.42485,115 7.57515,0 0,2.03613 -9,0 z"
+         id="rect10035"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="ccccc" />
+      <path
+         
style="color:#000000;fill:#50b6b1;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new"
+         d="m 501,115 10,0 0,2.03613 -10.40074,0 z"
+         id="rect10037"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="ccccc" />
+      <path
+         
style="color:#000000;fill:#e94055;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new"
+         d="m 511,115 10,0 0.44526,2.03613 -10.44526,0 z"
+         id="rect10039"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="ccccc" />
+      <path
+         
style="color:#000000;fill:#e8a945;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new"
+         d="m 521,115 7.57515,0 1.42485,2.03613 -8.59926,0 z"
+         id="rect10041"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="ccccc" />
+    </g>
+  </g>
+</svg>
diff --git a/src/pocket/gnome-pocket.c b/src/pocket/gnome-pocket.c
new file mode 100644
index 0000000..97ef8f5
--- /dev/null
+++ b/src/pocket/gnome-pocket.c
@@ -0,0 +1,934 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ *  Copyright © 2013 Bastien Nocera <hadess hadess net>
+ *
+ *  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 pocket 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * Implementation of:
+ * http://getpocket.com/developer/docs/overview
+ */
+
+//#include "config.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <glib/gstdio.h>
+#define GOA_API_IS_SUBJECT_TO_CHANGE 1
+#include <goa/goa.h>
+#include <rest/rest-proxy.h>
+#include <rest/rest-proxy-call.h>
+#include <json-glib/json-glib.h>
+
+#include "gnome-pocket.h"
+
+static gpointer
+gnome_pocket_item_copy (gpointer boxed)
+{
+  GnomePocketItem *src = boxed;
+
+  GnomePocketItem *dest = g_new (GnomePocketItem, 1);
+  dest->id = g_strdup (src->id);
+  dest->url = g_strdup (src->url);
+  dest->title = g_strdup (src->title);
+  dest->favorite = src->favorite;
+  dest->status = src->status;
+  dest->is_article = src->is_article;
+  dest->has_image = src->has_image;
+  dest->has_video = src->has_video;
+  dest->time_added = src->time_added;
+  dest->tags = g_strdupv (src->tags);
+
+  return dest;
+}
+
+static void
+gnome_pocket_item_free (gpointer boxed)
+{
+  GnomePocketItem *item = boxed;
+
+  g_free (item->id);
+  g_free (item->url);
+  g_free (item->title);
+  g_strfreev (item->tags);
+  g_free (item);
+}
+
+static char *
+get_string_for_element (JsonReader *reader,
+                        const char *element)
+{
+  char *ret;
+
+  if (!json_reader_read_member (reader, element)) {
+    json_reader_end_member (reader);
+    return NULL;
+  }
+  ret = g_strdup (json_reader_get_string_value (reader));
+  if (ret && *ret == '\0')
+    g_clear_pointer (&ret, g_free);
+  json_reader_end_member (reader);
+
+  return ret;
+}
+
+static int
+get_int_for_element (JsonReader *reader,
+                     const char *element)
+{
+  int ret;
+
+  if (!json_reader_read_member (reader, element)) {
+    json_reader_end_member (reader);
+    return -1;
+  }
+  ret = atoi (json_reader_get_string_value (reader));
+  json_reader_end_member (reader);
+
+  return ret;
+}
+
+static gint64
+get_time_added (JsonReader *reader)
+{
+  gint64 ret;
+
+  if (!json_reader_read_member (reader, "time_added")) {
+    json_reader_end_member (reader);
+    return -1;
+  }
+  ret = g_ascii_strtoll (json_reader_get_string_value (reader), NULL, 0);
+  json_reader_end_member (reader);
+
+  return ret;
+}
+
+static GnomePocketItem *
+parse_item (JsonReader *reader)
+{
+  GnomePocketItem *item;
+
+  item = g_new0 (GnomePocketItem, 1);
+  item->id = g_strdup (json_reader_get_member_name (reader));
+  if (!item->id)
+    goto bail;
+
+  /* If the item is archived or deleted, we don't need
+   * anything more here */
+  item->status = get_int_for_element (reader, "status");
+  if (item->status != POCKET_STATUS_NORMAL)
+    goto end;
+
+  item->url = get_string_for_element (reader, "resolved_url");
+  if (!item->url)
+    item->url = get_string_for_element (reader, "given_url");
+
+  item->title = get_string_for_element (reader, "resolved_title");
+  if (!item->title)
+    item->title = get_string_for_element (reader, "given_title");
+  if (!item->title)
+    item->title = g_strdup ("PLACEHOLDER"); /* FIXME generate from URL */
+
+  item->favorite = get_int_for_element (reader, "favorite");
+  item->is_article = get_int_for_element (reader, "is_article");
+  if (item->is_article == -1)
+    item->is_article = FALSE;
+  item->has_image = get_int_for_element (reader, "has_image");
+  if (item->has_image == -1)
+    item->has_image = POCKET_HAS_MEDIA_FALSE;
+  item->has_video = get_int_for_element (reader, "has_video");
+  if (item->has_video == -1)
+    item->has_video = POCKET_HAS_MEDIA_FALSE;
+
+  item->time_added = get_time_added (reader);
+
+#if 0
+  if (!json_reader_read_member (reader, "tags")) {
+    json_reader_end_member (reader);
+    goto bail;
+  }
+  item->tags = ;
+  json_reader_end_member (reader);
+#endif
+
+  goto end;
+
+bail:
+  g_clear_pointer (&item, gnome_pocket_item_free);
+
+end:
+  return item;
+}
+
+GnomePocketItem *
+gnome_pocket_item_from_string (const char *str)
+{
+  JsonParser *parser;
+  JsonReader *reader;
+  GnomePocketItem *item = NULL;
+  char **members = NULL;
+
+  parser = json_parser_new ();
+  if (!json_parser_load_from_data (parser, str, -1, NULL))
+    return NULL;
+
+  reader = json_reader_new (json_parser_get_root (parser));
+  members = json_reader_list_members (reader);
+  if (!members)
+    goto bail;
+
+  if (!json_reader_read_member (reader, members[0]))
+    goto bail;
+
+  item = parse_item (reader);
+
+bail:
+  g_clear_pointer (&members, g_strfreev);
+  g_clear_object (&reader);
+  g_clear_object (&parser);
+
+  return item;
+}
+
+static void
+builder_add_int_as_str (JsonBuilder *builder,
+                        const char  *name,
+                        int          value)
+{
+  char *tmp;
+
+  json_builder_set_member_name (builder, name);
+  tmp = g_strdup_printf ("%d", value);
+  json_builder_add_string_value (builder, tmp);
+  g_free (tmp);
+}
+
+static void
+builder_add_int64_as_str (JsonBuilder *builder,
+                          const char  *name,
+                          gint64       value)
+{
+  char *tmp;
+
+  json_builder_set_member_name (builder, name);
+  tmp = g_strdup_printf ("%" G_GINT64_FORMAT, value);
+  json_builder_add_string_value (builder, tmp);
+  g_free (tmp);
+}
+
+char *
+gnome_pocket_item_to_string (GnomePocketItem *item)
+{
+  char *ret = NULL;
+  JsonBuilder *builder;
+  JsonGenerator *gen;
+  JsonNode *root;
+
+  builder = json_builder_new ();
+
+  json_builder_begin_object (builder);
+  json_builder_set_member_name (builder, item->id);
+
+  json_builder_begin_object (builder);
+
+  json_builder_set_member_name (builder, "item_id");
+  json_builder_add_string_value (builder, item->id);
+
+  json_builder_set_member_name (builder, "resolved_url");
+  json_builder_add_string_value (builder, item->url);
+
+  json_builder_set_member_name (builder, "resolved_title");
+  json_builder_add_string_value (builder, item->title);
+
+  builder_add_int_as_str (builder, "favorite", item->favorite);
+  builder_add_int_as_str (builder, "status", item->status);
+  builder_add_int_as_str (builder, "is_article", item->is_article);
+  builder_add_int_as_str (builder, "has_image", item->has_image);
+  builder_add_int_as_str (builder, "has_video", item->has_video);
+  builder_add_int64_as_str (builder, "time_added", item->time_added);
+
+  json_builder_end_object (builder);
+  json_builder_end_object (builder);
+
+  gen = json_generator_new ();
+  root = json_builder_get_root (builder);
+  json_generator_set_root (gen, root);
+  ret = json_generator_to_data (gen, NULL);
+
+  json_node_free (root);
+  g_object_unref (gen);
+  g_object_unref (builder);
+
+  return ret;
+}
+
+G_DEFINE_BOXED_TYPE(GnomePocketItem, gnome_pocket_item, gnome_pocket_item_copy, gnome_pocket_item_free)
+
+enum {
+  PROP_0,
+  PROP_AVAILABLE
+};
+
+G_DEFINE_TYPE (GnomePocket, gnome_pocket, G_TYPE_OBJECT);
+
+#define GNOME_POCKET_GET_PRIVATE(object)(G_TYPE_INSTANCE_GET_PRIVATE ((object), GNOME_TYPE_POCKET, 
GnomePocketPrivate))
+
+struct _GnomePocketPrivate {
+  GCancellable   *cancellable;
+  GoaClient      *client;
+  GoaOAuth2Based *oauth2;
+  char           *access_token;
+  char           *consumer_key;
+  RestProxy      *proxy;
+
+  /* List data */
+  gboolean       cache_loaded;
+  gint64         since;
+  GList         *items; /* GnomePocketItem */
+};
+
+gboolean
+gnome_pocket_refresh_finish (GnomePocket       *self,
+                             GAsyncResult        *res,
+                             GError             **error)
+{
+  GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (res);
+  gboolean ret = FALSE;
+
+  g_warn_if_fail (g_simple_async_result_get_source_tag (simple) == gnome_pocket_refresh);
+
+  if (!g_simple_async_result_propagate_error (simple, error))
+    ret = g_simple_async_result_get_op_res_gboolean (simple);
+
+  return ret;
+}
+
+/* FIXME */
+static char *cache_path = NULL;
+
+static char *
+gnome_pocket_item_get_path (GnomePocketItem *item)
+{
+  g_return_val_if_fail (item != NULL, NULL);
+  return g_build_filename (cache_path, item->id, NULL);
+}
+
+static void
+gnome_pocket_item_save (GnomePocketItem *item)
+{
+  char *path;
+  char *str;
+
+  g_return_if_fail (item != NULL);
+
+  path = gnome_pocket_item_get_path (item);
+  str = gnome_pocket_item_to_string (item);
+  g_file_set_contents (path, str, -1, NULL);
+  g_free (str);
+  g_free (path);
+}
+
+static void
+gnome_pocket_item_remove (GnomePocketItem *item)
+{
+  char *path;
+
+  g_return_if_fail (item != NULL);
+
+  path = gnome_pocket_item_get_path (item);
+  g_unlink (path);
+  g_free (path);
+}
+
+static void
+update_list (GnomePocket *self,
+             GList       *updated_items)
+{
+  GHashTable *removed; /* key=id, value=gboolean */
+  GList *l;
+
+  if (updated_items == NULL)
+    return;
+
+  removed = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                   g_free, NULL);
+
+  for (l = updated_items; l != NULL; l = l->next) {
+    GnomePocketItem *item = l->data;
+
+    if (item->status != POCKET_STATUS_NORMAL) {
+      g_hash_table_insert (removed,
+                           g_strdup (item->id),
+                           GINT_TO_POINTER (1));
+      gnome_pocket_item_free (item);
+    } else {
+      self->priv->items = g_list_prepend (self->priv->items, item);
+      gnome_pocket_item_save (item);
+    }
+  }
+
+  /* And remove the old items */
+  for (l = self->priv->items; l != NULL; l = l->next) {
+    GnomePocketItem *item = l->data;
+
+    if (g_hash_table_lookup (removed, item->id)) {
+      /* Item got removed */
+      self->priv->items = g_list_delete_link (self->priv->items, l);
+
+      gnome_pocket_item_remove (item);
+      gnome_pocket_item_free (item);
+    }
+  }
+
+  g_hash_table_destroy (removed);
+}
+
+static gint64
+load_since (GnomePocket *self)
+{
+       char *path;
+       char *contents = NULL;
+       gint64 since = 0;
+
+       path = g_build_filename (cache_path, "since", NULL);
+       g_file_get_contents (path, &contents, NULL, NULL);
+       g_free (path);
+
+       if (contents != NULL) {
+               since = g_ascii_strtoll (contents, NULL, 0);
+               g_free (contents);
+       }
+
+       return since;
+}
+
+static void
+save_since (GnomePocket *self)
+{
+       char *str;
+       char *path;
+
+       if (self->priv->since == 0)
+               return;
+
+       str = g_strdup_printf ("%" G_GINT64_FORMAT, self->priv->since);
+       path = g_build_filename (cache_path, "since", NULL);
+       g_file_set_contents (path, str, -1, NULL);
+       g_free (path);
+       g_free (str);
+}
+
+static GList *
+parse_json (JsonParser *parser,
+            gint64     *since)
+{
+  JsonReader *reader;
+  GList *ret;
+  int num;
+
+  reader = json_reader_new (json_parser_get_root (parser));
+  *since = 0;
+  ret = NULL;
+
+  num = json_reader_count_members (reader);
+  if (num < 0)
+    goto bail;
+
+  /* Grab the since */
+  if (json_reader_read_member (reader, "since"))
+    *since = json_reader_get_int_value (reader);
+  json_reader_end_member (reader);
+
+  /* Grab the list */
+  if (json_reader_read_member (reader, "list")) {
+    char **members;
+    guint i;
+
+    members = json_reader_list_members (reader);
+    if (members != NULL) {
+      for (i = 0; members[i] != NULL; i++) {
+        GnomePocketItem *item;
+
+        if (!json_reader_read_member (reader, members[i])) {
+          json_reader_end_member (reader);
+          continue;
+        }
+        item = parse_item (reader);
+        if (item)
+          ret = g_list_prepend (ret, item);
+        json_reader_end_member (reader);
+      }
+    }
+    g_strfreev (members);
+  }
+  json_reader_end_member (reader);
+
+bail:
+  g_clear_object (&reader);
+  return ret;
+}
+
+static void
+refresh_cb (GObject      *object,
+            GAsyncResult *res,
+            gpointer      user_data)
+{
+  GError *error = NULL;
+  GSimpleAsyncResult *simple = user_data;
+  gboolean ret;
+
+  ret = rest_proxy_call_invoke_finish (REST_PROXY_CALL (object), res, &error);
+  if (!ret) {
+    g_simple_async_result_set_from_error (simple, error);
+  } else {
+    JsonParser *parser;
+
+    parser = json_parser_new ();
+    if (json_parser_load_from_data (parser,
+                                    rest_proxy_call_get_payload (REST_PROXY_CALL (object)),
+                                    rest_proxy_call_get_payload_length (REST_PROXY_CALL (object)),
+                                    NULL)) {
+      GList *updated_items;
+      GnomePocket *self;
+
+      self = GNOME_POCKET (g_async_result_get_source_object (G_ASYNC_RESULT (simple)));
+      updated_items = parse_json (parser, &self->priv->since);
+      if (self->priv->since != 0)
+        save_since (self);
+      update_list (self, updated_items);
+    }
+    g_object_unref (parser);
+  }
+  g_simple_async_result_set_op_res_gboolean (simple, ret);
+
+  g_simple_async_result_complete_in_idle (simple);
+  g_object_unref (simple);
+  g_clear_error (&error);
+}
+
+void
+gnome_pocket_refresh (GnomePocket         *self,
+                      GCancellable        *cancellable,
+                      GAsyncReadyCallback  callback,
+                      gpointer             user_data)
+{
+  RestProxyCall *call;
+  GSimpleAsyncResult *simple;
+
+  g_return_if_fail (GNOME_IS_POCKET (self));
+  g_return_if_fail (self->priv->consumer_key && self->priv->access_token);
+
+  simple = g_simple_async_result_new (G_OBJECT (self),
+                                      callback,
+                                      user_data,
+                                      gnome_pocket_refresh);
+
+  g_simple_async_result_set_check_cancellable (simple, cancellable);
+
+  call = rest_proxy_new_call (self->priv->proxy);
+  rest_proxy_call_set_method (call, "POST");
+  rest_proxy_call_set_function (call, "v3/get");
+  rest_proxy_call_add_param (call, "consumer_key", self->priv->consumer_key);
+  rest_proxy_call_add_param (call, "access_token", self->priv->access_token);
+
+  if (self->priv->since > 0) {
+    char *since;
+    since = g_strdup_printf ("%" G_GINT64_FORMAT, self->priv->since);
+    rest_proxy_call_add_param (call, "since", since);
+    g_free (since);
+  }
+
+  rest_proxy_call_invoke_async (call, cancellable, refresh_cb, simple);
+}
+
+gboolean
+gnome_pocket_add_url_finish (GnomePocket   *self,
+                             GAsyncResult  *res,
+                             GError       **error)
+{
+  GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (res);
+  gboolean ret = FALSE;
+
+  g_warn_if_fail (g_simple_async_result_get_source_tag (simple) == gnome_pocket_add_url);
+
+  if (!g_simple_async_result_propagate_error (simple, error))
+    ret = g_simple_async_result_get_op_res_gboolean (simple);
+
+  return ret;
+}
+
+static void
+add_url_cb (GObject      *object,
+            GAsyncResult *res,
+            gpointer      user_data)
+{
+  GError *error = NULL;
+  GSimpleAsyncResult *simple = user_data;
+  gboolean ret;
+
+  ret = rest_proxy_call_invoke_finish (REST_PROXY_CALL (object), res, &error);
+  if (!ret)
+    g_simple_async_result_set_from_error (simple, error);
+  g_simple_async_result_set_op_res_gboolean (simple, ret);
+
+  g_simple_async_result_complete_in_idle (simple);
+  g_object_unref (simple);
+  g_clear_error (&error);
+}
+
+void
+gnome_pocket_add_url (GnomePocket         *self,
+                      const char          *url,
+                      const char          *tweet_id,
+                      GCancellable        *cancellable,
+                      GAsyncReadyCallback  callback,
+                      gpointer             user_data)
+{
+  RestProxyCall *call;
+  GSimpleAsyncResult *simple;
+
+  g_return_if_fail (GNOME_IS_POCKET (self));
+  g_return_if_fail (url);
+  g_return_if_fail (self->priv->consumer_key && self->priv->access_token);
+
+  simple = g_simple_async_result_new (G_OBJECT (self),
+                                      callback,
+                                      user_data,
+                                      gnome_pocket_add_url);
+
+  g_simple_async_result_set_check_cancellable (simple, cancellable);
+
+  call = rest_proxy_new_call (self->priv->proxy);
+  rest_proxy_call_set_method (call, "POST");
+  rest_proxy_call_set_function (call, "v3/add");
+  rest_proxy_call_add_param (call, "consumer_key", self->priv->consumer_key);
+  rest_proxy_call_add_param (call, "access_token", self->priv->access_token);
+  rest_proxy_call_add_param (call, "url", url);
+  if (tweet_id)
+    rest_proxy_call_add_param (call, "tweet_id", tweet_id);
+
+  rest_proxy_call_invoke_async (call, cancellable, add_url_cb, simple);
+}
+
+static void
+parse_cached_data (GnomePocket *self)
+{
+  GDir *dir;
+  const char *name;
+
+  dir = g_dir_open (cache_path, 0, NULL);
+  if (!dir)
+    return;
+
+  self->priv->since = load_since (self);
+
+  name = g_dir_read_name (dir);
+  while (name) {
+    JsonParser *parser;
+    JsonReader *reader;
+    char *full_path = NULL;
+    char **members;
+    GnomePocketItem *item;
+
+    if (g_strcmp0 (name, "since") == 0)
+      goto next;
+
+    full_path = g_build_filename (cache_path, name, NULL);
+    parser = json_parser_new ();
+    if (!json_parser_load_from_file (parser, full_path, NULL))
+      goto next;
+
+    reader = json_reader_new (json_parser_get_root (parser));
+    members = json_reader_list_members (reader);
+    if (!members)
+      goto next;
+
+    if (!json_reader_read_member (reader, members[0]))
+      goto next;
+
+    item = parse_item (reader);
+    if (!item)
+      g_warning ("Could not parse cached file '%s'", full_path);
+    else
+      self->priv->items = g_list_prepend (self->priv->items, item);
+
+next:
+    g_clear_object (&reader);
+    g_clear_object (&parser);
+    g_free (full_path);
+
+    name = g_dir_read_name (dir);
+  }
+  g_dir_close (dir);
+}
+
+static void
+load_cached_thread (GTask           *task,
+                   gpointer         source_object,
+                   gpointer         task_data,
+                   GCancellable    *cancellable)
+{
+  GnomePocket *self = GNOME_POCKET (source_object);
+
+  parse_cached_data (self);
+  self->priv->cache_loaded = TRUE;
+  g_task_return_boolean (task, TRUE);
+}
+
+void
+gnome_pocket_load_cached (GnomePocket         *self,
+                         GCancellable        *cancellable,
+                         GAsyncReadyCallback  callback,
+                         gpointer             user_data)
+{
+  GTask *task;
+
+  g_return_if_fail (GNOME_IS_POCKET (self));
+  g_return_if_fail (!self->priv->cache_loaded);
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_run_in_thread (task, load_cached_thread);
+  g_object_unref (task);
+}
+
+gboolean
+gnome_pocket_load_cached_finish (GnomePocket         *self,
+                                GAsyncResult        *res,
+                                GError             **error)
+{
+  GTask *task = G_TASK (res);
+
+  g_return_val_if_fail (g_task_is_valid (res, self), NULL);
+
+  return g_task_propagate_boolean (task, error);
+}
+
+GList *
+gnome_pocket_get_items (GnomePocket *self)
+{
+  g_return_val_if_fail (self->priv->cache_loaded, NULL);
+  return self->priv->items;
+}
+
+static void
+gnome_pocket_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
+{
+  GnomePocket *self = GNOME_POCKET (object);
+
+  switch (property_id) {
+  case PROP_AVAILABLE:
+    g_value_set_boolean (value, self->priv->access_token && self->priv->consumer_key);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (self, property_id, pspec);
+    break;
+  }
+}
+
+static void
+gnome_pocket_finalize (GObject *object)
+{
+  GnomePocketPrivate *priv = GNOME_POCKET (object)->priv;
+
+  g_cancellable_cancel (priv->cancellable);
+  g_clear_object (&priv->cancellable);
+
+  g_clear_object (&priv->proxy);
+  g_clear_object (&priv->oauth2);
+  g_clear_object (&priv->client);
+  g_clear_pointer (&priv->access_token, g_free);
+  g_clear_pointer (&priv->consumer_key, g_free);
+
+  G_OBJECT_CLASS (gnome_pocket_parent_class)->finalize (object);
+}
+
+static void
+gnome_pocket_class_init (GnomePocketClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  cache_path = g_build_filename (g_get_user_cache_dir (), "pocket", NULL);
+  g_mkdir_with_parents (cache_path, 0700);
+
+  object_class->get_property = gnome_pocket_get_property;
+  object_class->finalize = gnome_pocket_finalize;
+
+  g_object_class_install_property (object_class,
+                                   PROP_AVAILABLE,
+                                   g_param_spec_boolean ("available",
+                                                         "Available",
+                                                         "If Read Pocket is available",
+                                                         FALSE,
+                                                         G_PARAM_READABLE | G_PARAM_STATIC_NAME | 
G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));
+
+  g_type_class_add_private (object_class, sizeof (GnomePocketPrivate));
+}
+
+static void
+got_access_token (GObject       *object,
+                  GAsyncResult  *res,
+                  GnomePocket *self)
+{
+  GError *error = NULL;
+  char *access_token;
+
+  if (!goa_oauth2_based_call_get_access_token_finish (GOA_OAUTH2_BASED (object),
+                                                      &access_token,
+                                                      NULL,
+                                                      res,
+                                                      &error)) {
+    if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+      g_error_free (error);
+      return;
+    }
+    g_warning ("Failed to get access token: %s", error->message);
+    g_error_free (error);
+    return;
+  }
+
+  self->priv->access_token = access_token;
+  self->priv->consumer_key = goa_oauth2_based_dup_client_id (GOA_OAUTH2_BASED (object));
+
+  g_object_notify (G_OBJECT (self), "available");
+}
+
+static void
+handle_accounts (GnomePocket *self)
+{
+  GList *accounts, *l;
+  GoaOAuth2Based *oauth2 = NULL;
+
+  g_clear_object (&self->priv->oauth2);
+  g_clear_pointer (&self->priv->access_token, g_free);
+  g_clear_pointer (&self->priv->consumer_key, g_free);
+
+  accounts = goa_client_get_accounts (self->priv->client);
+
+  for (l = accounts; l != NULL; l = l->next) {
+    GoaObject *object = GOA_OBJECT (l->data);
+    GoaAccount *account;
+
+    account = goa_object_peek_account (object);
+
+    /* Find a Pocket account that doesn't have "Read Pocket" disabled */
+    if (g_strcmp0 (goa_account_get_provider_type (account), "pocket") == 0 &&
+        !goa_account_get_read_later_disabled (account)) {
+      oauth2 = goa_object_get_oauth2_based (object);
+      break;
+    }
+  }
+
+  g_list_free_full (accounts, (GDestroyNotify) g_object_unref);
+
+  if (!oauth2) {
+    g_object_notify (G_OBJECT (self), "available");
+    g_debug ("Could not find a Pocket account");
+    return;
+  }
+
+  self->priv->oauth2 = oauth2;
+
+  goa_oauth2_based_call_get_access_token (oauth2,
+                                          self->priv->cancellable,
+                                          (GAsyncReadyCallback) got_access_token,
+                                          self);
+}
+
+static void
+account_added_cb (GoaClient     *client,
+                  GoaObject     *object,
+                  GnomePocket *self)
+{
+  if (self->priv->oauth2 != NULL) {
+    /* Don't care, already have an account */
+    return;
+  }
+
+  handle_accounts (self);
+}
+
+static void
+account_changed_cb (GoaClient     *client,
+                    GoaObject     *object,
+                    GnomePocket *self)
+{
+  GoaOAuth2Based *oauth2;
+
+  oauth2 = goa_object_get_oauth2_based (object);
+  if (oauth2 == self->priv->oauth2)
+    handle_accounts (self);
+
+  g_object_unref (oauth2);
+}
+
+static void
+account_removed_cb (GoaClient     *client,
+                    GoaObject     *object,
+                    GnomePocket *self)
+{
+  GoaOAuth2Based *oauth2;
+
+  oauth2 = goa_object_get_oauth2_based (object);
+  if (oauth2 == self->priv->oauth2)
+    handle_accounts (self);
+
+  g_object_unref (oauth2);
+}
+
+static void
+client_ready_cb (GObject       *source_object,
+                 GAsyncResult  *res,
+                 GnomePocket *self)
+{
+  GoaClient *client;
+  GError *error = NULL;
+
+  client = goa_client_new_finish (res, &error);
+  if (client == NULL) {
+    if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+      g_error_free (error);
+      return;
+    }
+    g_warning ("Failed to get GoaClient: %s", error->message);
+    g_error_free (error);
+    return;
+  }
+
+  self->priv->client = client;
+  g_signal_connect (self->priv->client, "account-added",
+                    G_CALLBACK (account_added_cb), self);
+  g_signal_connect (self->priv->client, "account-changed",
+                    G_CALLBACK (account_changed_cb), self);
+  g_signal_connect (self->priv->client, "account-removed",
+                    G_CALLBACK (account_removed_cb), self);
+
+  handle_accounts (self);
+}
+
+static void
+gnome_pocket_init (GnomePocket *self)
+{
+  self->priv = GNOME_POCKET_GET_PRIVATE (self);
+  self->priv->cancellable = g_cancellable_new ();
+  self->priv->proxy = rest_proxy_new ("https://getpocket.com/";, FALSE);
+
+  goa_client_new (self->priv->cancellable,
+                  (GAsyncReadyCallback) client_ready_cb, self);
+}
+
+GnomePocket *
+gnome_pocket_new (void)
+{
+  return g_object_new (GNOME_TYPE_POCKET, NULL);
+}
diff --git a/src/pocket/gnome-pocket.h b/src/pocket/gnome-pocket.h
new file mode 100644
index 0000000..1f16025
--- /dev/null
+++ b/src/pocket/gnome-pocket.h
@@ -0,0 +1,117 @@
+/*
+ *  Copyright © 2013 Bastien Nocera <hadess hadess net>
+ *
+ *  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 pocket 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+#ifndef GNOME_POCKET_H
+#define GNOME_POCKET_H
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define POCKET_FAVORITE TRUE
+#define POCKET_NOT_FAVORITE FALSE
+
+typedef enum {
+  POCKET_STATUS_NORMAL    = 0,
+  POCKET_STATUS_ARCHIVED  = 1,
+  POCKET_STATUS_DELETED   = 2
+} PocketItemStatus;
+
+#define POCKET_IS_ARTICLE TRUE
+#define POCKET_IS_NOT_ARTICLE FALSE
+
+typedef enum {
+  POCKET_HAS_MEDIA_FALSE     = 0,
+  POCKET_HAS_MEDIA_INCLUDED  = 1,
+  POCKET_IS_MEDIA            = 2
+} PocketMediaInclusion;
+
+typedef struct {
+  char                  *id;
+  char                  *url;
+  char                  *title;
+  gboolean               favorite;
+  PocketItemStatus       status;
+  gboolean               is_article;
+  PocketMediaInclusion   has_image;
+  PocketMediaInclusion   has_video;
+  gint64                 time_added;
+  char                 **tags;
+} GnomePocketItem;
+
+#define GNOME_TYPE_POCKET_ITEM (gnome_pocket_item_get_type ())
+
+GType            gnome_pocket_item_get_type    (void) G_GNUC_CONST;
+char            *gnome_pocket_item_to_string   (GnomePocketItem *item);
+GnomePocketItem *gnome_pocket_item_from_string (const char *str);
+
+#define GNOME_TYPE_POCKET         (gnome_pocket_get_type ())
+#define GNOME_POCKET(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), GNOME_TYPE_POCKET, GnomePocket))
+#define GNOME_POCKET_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), GNOME_TYPE_POCKET, GnomePocketClass))
+#define GNOME_IS_POCKET(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), GNOME_TYPE_POCKET))
+#define GNOME_IS_POCKET_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), GNOME_TYPE_POCKET))
+#define GNOME_POCKET_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GNOME_TYPE_POCKET, GnomePocketClass))
+
+typedef struct _GnomePocketPrivate GnomePocketPrivate;
+
+typedef struct
+{
+  GObject parent;
+
+  /*< private >*/
+  GnomePocketPrivate *priv;
+} GnomePocket;
+
+typedef struct
+{
+  GObjectClass parent;
+} GnomePocketClass;
+
+GType              gnome_pocket_get_type        (void);
+
+GnomePocket       *gnome_pocket_new             (void);
+void               gnome_pocket_add_url         (GnomePocket         *self,
+                                                 const char          *url,
+                                                 const char          *tweet_id,
+                                                 GCancellable        *cancellable,
+                                                 GAsyncReadyCallback  callback,
+                                                 gpointer             user_data);
+gboolean           gnome_pocket_add_url_finish  (GnomePocket         *self,
+                                                 GAsyncResult        *res,
+                                                 GError             **error);
+void               gnome_pocket_refresh         (GnomePocket         *self,
+                                                 GCancellable        *cancellable,
+                                                 GAsyncReadyCallback  callback,
+                                                 gpointer             user_data);
+gboolean           gnome_pocket_refresh_finish  (GnomePocket         *self,
+                                                 GAsyncResult        *res,
+                                                 GError             **error);
+GList             *gnome_pocket_get_items       (GnomePocket         *self);
+
+void               gnome_pocket_load_cached        (GnomePocket         *self,
+                                                    GCancellable        *cancellable,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+gboolean           gnome_pocket_load_cached_finish (GnomePocket         *self,
+                                                    GAsyncResult        *res,
+                                                    GError             **error);
+
+G_END_DECLS
+
+#endif /* GNOME_POCKET_H */
diff --git a/src/pocket/grl-pocket.c b/src/pocket/grl-pocket.c
new file mode 100644
index 0000000..99b2578
--- /dev/null
+++ b/src/pocket/grl-pocket.c
@@ -0,0 +1,405 @@
+/*
+ * Copyright (C) 2013 Bastien Nocera
+ *
+ * Contact: Bastien Nocera <hadess hadess net>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <grilo.h>
+#include <gio/gio.h>
+#include <glib/gi18n-lib.h>
+#include <string.h>
+#include <stdlib.h>
+#include <totem-pl-parser-mini.h>
+
+#include "grl-pocket.h"
+#include "gnome-pocket.h"
+
+/* --------- Logging  -------- */
+
+#define GRL_LOG_DOMAIN_DEFAULT pocket_log_domain
+GRL_LOG_DOMAIN_STATIC(pocket_log_domain);
+
+/* --- Plugin information --- */
+
+#define PLUGIN_ID   POCKET_PLUGIN_ID
+
+#define SOURCE_ID   "grl-pocket"
+#define SOURCE_NAME _("Pocket")
+#define SOURCE_DESC _("A source for browsing Pocket videos")
+
+/* --- Grilo Pocket Private --- */
+
+#define GRL_POCKET_SOURCE_GET_PRIVATE(object)           \
+  (G_TYPE_INSTANCE_GET_PRIVATE((object),                       \
+                               GRL_POCKET_SOURCE_TYPE,  \
+                               GrlPocketSourcePrivate))
+
+struct _GrlPocketSourcePrivate {
+  GnomePocket *pocket;
+  gboolean     cache_loaded;
+};
+
+/* --- Data types --- */
+
+typedef struct {
+  GrlSourceBrowseSpec *bs;
+  GCancellable        *cancellable;
+  GrlPocketSource     *source;
+} OperationData;
+
+static GrlPocketSource *grl_pocket_source_new (GnomePocket *pocket);
+
+static void grl_pocket_source_finalize (GObject *object);
+
+gboolean grl_pocket_plugin_init (GrlRegistry *registry,
+                                 GrlPlugin   *plugin,
+                                 GList       *configs);
+
+static const GList *grl_pocket_source_supported_keys (GrlSource *source);
+
+static void grl_pocket_source_cancel (GrlSource *source,
+                                      guint      operation_id);
+static void grl_pocket_source_browse (GrlSource           *source,
+                                       GrlSourceBrowseSpec *bs);
+
+/* =================== Pocket Plugin  =============== */
+
+static void
+is_available (GObject    *gobject,
+              GParamSpec *pspec,
+              GrlPlugin  *plugin)
+{
+  gboolean avail;
+  GrlPocketSource *source;
+  GnomePocket *pocket;
+
+  source = g_object_get_data (G_OBJECT (plugin), "source");
+  pocket = g_object_get_data (G_OBJECT (plugin), "pocket");
+  g_object_get (pocket, "available", &avail, NULL);
+
+  if (!avail) {
+    GrlRegistry *registry;
+
+    if (source == NULL)
+      return;
+
+    GRL_DEBUG ("Removing Pocket");
+
+    registry = grl_registry_get_default ();
+
+    grl_registry_unregister_source (registry,
+                                    GRL_SOURCE (source),
+                                    NULL);
+  } else {
+    GrlRegistry *registry;
+
+    if (source != NULL)
+      return;
+
+    GRL_DEBUG ("Adding Pocket");
+
+    source = grl_pocket_source_new (pocket);
+    registry = grl_registry_get_default ();
+
+    g_object_set_data (G_OBJECT (plugin), "source", source);
+    grl_registry_register_source (registry,
+                                  plugin,
+                                  GRL_SOURCE (source),
+                                  NULL);
+  }
+}
+
+gboolean
+grl_pocket_plugin_init (GrlRegistry *registry,
+                        GrlPlugin   *plugin,
+                        GList       *configs)
+{
+  GnomePocket *pocket;
+
+  GRL_LOG_DOMAIN_INIT (pocket_log_domain, "pocket");
+
+  GRL_DEBUG ("%s", __FUNCTION__);
+
+  /* Initialize i18n */
+  bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+  bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+
+  pocket = gnome_pocket_new ();
+  g_object_set_data (G_OBJECT (plugin), "pocket", pocket);
+  g_signal_connect (pocket, "notify::available",
+                    G_CALLBACK (is_available), plugin);
+
+  return TRUE;
+}
+
+static void
+grl_pocket_plugin_deinit (GrlPlugin *plugin)
+{
+  GnomePocket *pocket;
+
+  GRL_DEBUG ("grl_pocket_plugin_deinit");
+
+  pocket = g_object_get_data (G_OBJECT (plugin), "pocket");
+  g_clear_object (&pocket);
+  g_object_set_data (G_OBJECT (plugin), "pocket", NULL);
+}
+
+GRL_PLUGIN_REGISTER (grl_pocket_plugin_init,
+                     grl_pocket_plugin_deinit,
+                     PLUGIN_ID);
+
+/* ================== Pocket GObject ================ */
+
+
+G_DEFINE_TYPE (GrlPocketSource,
+               grl_pocket_source,
+               GRL_TYPE_SOURCE);
+
+static GrlPocketSource *
+grl_pocket_source_new (GnomePocket *pocket)
+{
+  GIcon *icon;
+  GFile *file;
+  GrlPocketSource *object;
+
+  g_return_val_if_fail (GNOME_IS_POCKET (pocket), NULL);
+
+  GRL_DEBUG ("%s", __FUNCTION__);
+
+  file = g_file_new_for_uri ("resource:///org/gnome/grilo/plugins/pocket/channel-pocket.svg");
+  icon = g_file_icon_new (file);
+  g_object_unref (file);
+  object = g_object_new (GRL_POCKET_SOURCE_TYPE,
+                         "source-id", SOURCE_ID,
+                         "source-name", SOURCE_NAME,
+                         "source-desc", SOURCE_DESC,
+                         "supported-media", GRL_MEDIA_TYPE_VIDEO,
+                         "source-icon", icon,
+                         NULL);
+  GRL_POCKET_SOURCE (object)->priv->pocket = pocket;
+
+  return object;
+}
+
+static void
+grl_pocket_source_class_init (GrlPocketSourceClass * klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GrlSourceClass *source_class = GRL_SOURCE_CLASS (klass);
+
+  object_class->finalize = grl_pocket_source_finalize;
+
+  source_class->supported_keys = grl_pocket_source_supported_keys;
+  source_class->browse = grl_pocket_source_browse;
+  source_class->cancel = grl_pocket_source_cancel;
+
+  g_type_class_add_private (klass, sizeof (GrlPocketSourcePrivate));
+}
+
+static void
+grl_pocket_source_init (GrlPocketSource *source)
+{
+  source->priv = GRL_POCKET_SOURCE_GET_PRIVATE(source);
+}
+
+static void
+grl_pocket_source_finalize (GObject *object)
+{
+  GrlPocketSource *source = GRL_POCKET_SOURCE (object);
+  GrlPocketSourcePrivate *priv = GRL_POCKET_SOURCE (source)->priv;
+
+  G_OBJECT_CLASS (grl_pocket_source_parent_class)->finalize (object);
+}
+
+/* ================== API Implementation ================ */
+
+static const GList *
+grl_pocket_source_supported_keys (GrlSource *source)
+{
+  static GList *keys = NULL;
+  if (!keys) {
+    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
+                                      GRL_METADATA_KEY_TITLE,
+                                      GRL_METADATA_KEY_URL,
+                                      GRL_METADATA_KEY_FAVOURITE,
+                                      GRL_METADATA_KEY_CREATION_DATE,
+                                      NULL);
+  }
+  return keys;
+}
+
+static GrlMedia *
+item_to_media (GnomePocketItem *item)
+{
+  GrlMedia *ret;
+  GDateTime *date;
+  gboolean has_video;
+
+  /* There are a few false positives, but this makes detection
+   * much faster as well */
+  has_video = (item->has_video == POCKET_HAS_MEDIA_INCLUDED ||
+               item->has_video == POCKET_IS_MEDIA);
+  if (!has_video) {
+    GRL_DEBUG ("Ignoring ID %s as it wasn't detected as a video, or as including a video (URL: %s)",
+               item->id, item->url);
+    return NULL;
+  }
+
+  if (!totem_pl_parser_can_parse_from_uri (item->url, FALSE)) {
+    GRL_DEBUG ("Ignoring ID %s as it wasn't detected as from a videosite (URL: %s)",
+               item->id, item->url);
+    return NULL;
+  }
+
+  ret = grl_media_video_new ();
+  grl_media_set_url (ret, item->url);
+  grl_media_set_title (ret, item->title);
+  grl_media_set_favourite (ret, item->favorite);
+
+  date = g_date_time_new_from_unix_utc (item->time_added);
+  grl_media_set_creation_date (ret, date);
+  g_date_time_unref (date);
+
+  return ret;
+}
+
+static void
+refresh_cb (GObject      *object,
+            GAsyncResult *res,
+            gpointer      user_data)
+{
+  OperationData *op_data = user_data;
+  GError *error = NULL;
+  GList *items, *l;
+
+  if (gnome_pocket_refresh_finish (op_data->source->priv->pocket,
+                                   res, &error) == FALSE) {
+    if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+      goto out;
+    }
+
+    op_data->bs->callback (op_data->bs->source,
+                           op_data->bs->operation_id,
+                           NULL,
+                           0,
+                           op_data->bs->user_data,
+                           error);
+    goto out;
+  }
+
+  items = gnome_pocket_get_items (op_data->source->priv->pocket);
+  for (l = items; l != NULL; l = l->next) {
+    GnomePocketItem *item = l->data;
+    GrlMedia *media;
+
+    media = item_to_media (item);
+    if (media == NULL)
+      continue;
+
+    op_data->bs->callback (op_data->bs->source,
+                           op_data->bs->operation_id,
+                           media,
+                           GRL_SOURCE_REMAINING_UNKNOWN,
+                           op_data->bs->user_data,
+                           NULL);
+  }
+
+  op_data->bs->callback (op_data->bs->source,
+                         op_data->bs->operation_id,
+                         NULL,
+                         0,
+                         op_data->bs->user_data,
+                         NULL);
+
+out:
+  g_clear_object (&op_data->cancellable);
+  g_slice_free (OperationData, op_data);
+}
+
+static void
+load_cached_cb (GObject      *object,
+                GAsyncResult *res,
+                gpointer      user_data)
+{
+  OperationData *op_data = user_data;
+  GError *error = NULL;
+
+  if (gnome_pocket_load_cached_finish (op_data->source->priv->pocket,
+                                       res, &error) == FALSE) {
+    if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+      goto out;
+    }
+  }
+
+  op_data->source->priv->cache_loaded = TRUE;
+  gnome_pocket_refresh (op_data->source->priv->pocket,
+                        op_data->cancellable,
+                        refresh_cb,
+                        op_data);
+  return;
+
+out:
+  g_clear_object (&op_data->cancellable);
+  g_slice_free (OperationData, op_data);
+}
+
+static void
+grl_pocket_source_browse (GrlSource           *source,
+                          GrlSourceBrowseSpec *bs)
+{
+  GrlPocketSourcePrivate *priv = GRL_POCKET_SOURCE (source)->priv;
+  OperationData *op_data;
+
+  GRL_DEBUG (__FUNCTION__);
+
+  op_data = g_slice_new0 (OperationData);
+  op_data->bs = bs;
+  op_data->cancellable = g_cancellable_new ();
+  op_data->source = GRL_POCKET_SOURCE (source);
+  grl_operation_set_data (bs->operation_id, op_data);
+
+  if (!priv->cache_loaded) {
+    gnome_pocket_load_cached (priv->pocket,
+                              op_data->cancellable,
+                              load_cached_cb,
+                              op_data);
+  } else {
+    gnome_pocket_refresh (priv->pocket,
+                          op_data->cancellable,
+                          refresh_cb,
+                          op_data);
+  }
+}
+
+static void
+grl_pocket_source_cancel (GrlSource *source,
+                          guint      operation_id)
+{
+  OperationData *op_data;
+
+  GRL_DEBUG ("grl_pocket_source_cancel");
+
+  op_data = (OperationData *) grl_operation_get_data (operation_id);
+  if (op_data)
+    g_cancellable_cancel (op_data->cancellable);
+}
diff --git a/src/pocket/grl-pocket.h b/src/pocket/grl-pocket.h
new file mode 100644
index 0000000..7c51530
--- /dev/null
+++ b/src/pocket/grl-pocket.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2013 Bastien Nocera
+ *
+ * Contact: Bastien Nocera <hadess hadess net>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ *
+ */
+
+#ifndef _GRL_POCKET_SOURCE_H_
+#define _GRL_POCKET_SOURCE_H_
+
+#include <grilo.h>
+
+#define GRL_POCKET_SOURCE_TYPE           \
+  (grl_pocket_source_get_type ())
+
+#define GRL_POCKET_SOURCE(obj)                          \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                          \
+                               GRL_POCKET_SOURCE_TYPE,  \
+                               GrlPocketSource))
+
+#define GRL_IS_POCKET_SOURCE(obj)                       \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                          \
+                               GRL_POCKET_SOURCE_TYPE))
+
+#define GRL_POCKET_SOURCE_CLASS(klass)               \
+  (G_TYPE_CHECK_CLASS_CAST((klass),                         \
+                           GRL_POCKET_SOURCE_TYPE,   \
+                           GrlPocketSourceClass))
+
+#define GRL_IS_POCKET_SOURCE_CLASS(klass)            \
+  (G_TYPE_CHECK_CLASS_TYPE((klass)                          \
+                           GRL_POCKET_SOURCE_TYPE))
+
+#define GRL_POCKET_SOURCE_GET_CLASS(obj)                \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                           \
+                              GRL_POCKET_SOURCE_TYPE,   \
+                              GrlPocketSourceClass))
+
+
+typedef struct _GrlPocketSource GrlPocketSource;
+typedef struct _GrlPocketSourcePrivate GrlPocketSourcePrivate;
+
+struct _GrlPocketSource {
+
+  GrlSource parent;
+
+  /*< private >*/
+  GrlPocketSourcePrivate *priv;
+};
+
+typedef struct _GrlPocketSourceClass GrlPocketSourceClass;
+
+struct _GrlPocketSourceClass {
+
+  GrlSourceClass parent_class;
+
+};
+
+GType grl_pocket_source_get_type (void);
+
+#endif /* _GRL_POCKET_SOURCE_H_ */
diff --git a/src/pocket/grl-pocket.xml b/src/pocket/grl-pocket.xml
new file mode 100644
index 0000000..b841836
--- /dev/null
+++ b/src/pocket/grl-pocket.xml
@@ -0,0 +1,10 @@
+<plugin>
+  <info>
+    <name>Pocket</name>
+    <module>libgrlpocket</module>
+    <description>A plugin for Pocket videos</description>
+    <author>GNOME</author>
+    <license>LGPL</license>
+    <site>http://www.gnome.org</site>
+  </info>
+</plugin>
diff --git a/src/pocket/pocket.gresource.xml b/src/pocket/pocket.gresource.xml
new file mode 100644
index 0000000..1f7d95c
--- /dev/null
+++ b/src/pocket/pocket.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/grilo/plugins/pocket">
+    <file compressed="false">channel-pocket.svg</file>
+  </gresource>
+</gresources>


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