[gnome-software/wip/hughsie/steam2] Add Steam support
- From: Richard Hughes <rhughes src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-software/wip/hughsie/steam2] Add Steam support
- Date: Wed, 2 Mar 2016 17:18:37 +0000 (UTC)
commit 8832be242a3aa683901c319aead140ed994484df
Author: Richard Hughes <richard hughsie com>
Date: Wed Mar 2 16:38:20 2016 +0000
Add Steam support
configure.ac | 1 +
contrib/gnome-software.spec.in | 1 +
src/plugins/Makefile.am | 10 +
src/plugins/gs-html-utils.c | 246 +++++++++
src/plugins/gs-html-utils.h | 34 ++
src/plugins/gs-plugin-steam.c | 1079 ++++++++++++++++++++++++++++++++++++++++
src/plugins/gs-self-test.c | 39 ++
7 files changed, 1410 insertions(+), 0 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 7436e8e..2aa7c4b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -65,6 +65,7 @@ PKG_CHECK_MODULES(APPSTREAM, appstream-glib >= 0.5.11)
PKG_CHECK_MODULES(GDK_PIXBUF, gdk-pixbuf-2.0 >= 2.31.5)
PKG_CHECK_MODULES(JSON_GLIB, json-glib-1.0 >= 1.1.1)
PKG_CHECK_MODULES(SQLITE, sqlite3)
+PKG_CHECK_MODULES(ICNS, libicns)
PKG_CHECK_MODULES(SOUP, libsoup-2.4 >= 2.51.92)
PKG_CHECK_MODULES(GSETTINGS_DESKTOP_SCHEMAS, gsettings-desktop-schemas >= 3.11.5)
PKG_CHECK_MODULES(GNOME_DESKTOP, gnome-desktop-3.0 >= 3.17.92)
diff --git a/contrib/gnome-software.spec.in b/contrib/gnome-software.spec.in
index 8b7e866..ff58e2c 100644
--- a/contrib/gnome-software.spec.in
+++ b/contrib/gnome-software.spec.in
@@ -28,6 +28,7 @@ BuildRequires: libnotify-devel
BuildRequires: PackageKit-glib-devel >= 0.8.10
BuildRequires: libsoup-devel
BuildRequires: gnome-desktop3-devel
+BuildRequires: libicns-devel
BuildRequires: gsettings-desktop-schemas-devel
BuildRequires: libappstream-glib-devel
BuildRequires: fwupd-devel
diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am
index a2964b8..f248317 100644
--- a/src/plugins/Makefile.am
+++ b/src/plugins/Makefile.am
@@ -42,6 +42,7 @@ plugin_LTLIBRARIES = \
libgs_plugin_fedora_tagger_usage.la \
libgs_plugin_shell-extensions.la \
libgs_plugin_epiphany.la \
+ libgs_plugin_steam.la \
libgs_plugin_icons.la
if HAVE_PACKAGEKIT
@@ -69,6 +70,14 @@ if HAVE_LIMBA
plugin_LTLIBRARIES += libgs_plugin_limba.la
endif
+libgs_plugin_steam_la_SOURCES = \
+ gs-html-utils.c \
+ gs-html-utils.h \
+ gs-plugin-steam.c
+libgs_plugin_steam_la_LIBADD = $(GS_PLUGIN_LIBS) $(ICNS_LIBS)
+libgs_plugin_steam_la_LDFLAGS = -module -avoid-version
+libgs_plugin_steam_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
+
libgs_plugin_dummy_la_SOURCES = gs-plugin-dummy.c
libgs_plugin_dummy_la_LIBADD = $(GS_PLUGIN_LIBS)
libgs_plugin_dummy_la_LDFLAGS = -module -avoid-version
@@ -236,6 +245,7 @@ check_PROGRAMS = \
gs-self-test
gs_self_test_SOURCES = \
+ gs-html-utils.c \
gs-moduleset.c \
gs-self-test.c
diff --git a/src/plugins/gs-html-utils.c b/src/plugins/gs-html-utils.c
new file mode 100644
index 0000000..b33d9e3
--- /dev/null
+++ b/src/plugins/gs-html-utils.c
@@ -0,0 +1,246 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2014 Richard Hughes <richard hughsie com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <glib.h>
+#include <appstream-glib.h>
+
+#include "gs-html-utils.h"
+
+typedef enum {
+ GS_HTML_UTILS_ACTION_IGNORE,
+ GS_HTML_UTILS_ACTION_PARA,
+ GS_HTML_UTILS_ACTION_LI,
+ GS_HTML_UTILS_ACTION_LAST
+} GsHtmlUtilsAction;
+
+typedef struct {
+ GsHtmlUtilsAction action;
+ GString *str;
+} GsHtmlUtilsHelper;
+
+/**
+ * gs_html_utils_start_cb:
+ **/
+static void
+gs_html_utils_start_cb (GMarkupParseContext *context,
+ const gchar *element_name,
+ const gchar **attribute_names,
+ const gchar **attribute_values,
+ gpointer user_data,
+ GError **error)
+{
+ GsHtmlUtilsHelper *helper = (GsHtmlUtilsHelper *) user_data;
+ if (g_strcmp0 (element_name, "book") == 0)
+ return;
+ if (g_strcmp0 (element_name, "li") == 0) {
+ helper->action = GS_HTML_UTILS_ACTION_LI;
+ return;
+ }
+ if (g_strcmp0 (element_name, "p") == 0) {
+ helper->action = GS_HTML_UTILS_ACTION_PARA;
+ return;
+ }
+ if (g_strcmp0 (element_name, "ul") == 0 ||
+ g_strcmp0 (element_name, "ol") == 0) {
+ g_string_append (helper->str, "<ul>");
+ return;
+ }
+ g_warning ("unhandled START %s", element_name);
+}
+
+/**
+ * gs_html_utils_end_cb:
+ **/
+static void
+gs_html_utils_end_cb (GMarkupParseContext *context,
+ const gchar *element_name,
+ gpointer user_data,
+ GError **error)
+{
+ GsHtmlUtilsHelper *helper = (GsHtmlUtilsHelper *) user_data;
+ if (g_strcmp0 (element_name, "book") == 0 ||
+ g_strcmp0 (element_name, "li") == 0) {
+ return;
+ }
+ if (g_strcmp0 (element_name, "p") == 0) {
+ helper->action = GS_HTML_UTILS_ACTION_IGNORE;
+ return;
+ }
+ if (g_strcmp0 (element_name, "ul") == 0 ||
+ g_strcmp0 (element_name, "ol") == 0) {
+ g_string_append (helper->str, "</ul>");
+ return;
+ }
+ g_warning ("unhandled END %s", element_name);
+}
+
+/**
+ * gs_html_utils_text_cb:
+ **/
+static void
+gs_html_utils_text_cb (GMarkupParseContext *context,
+ const gchar *text,
+ gsize text_len,
+ gpointer user_data,
+ GError **error)
+{
+ GsHtmlUtilsHelper *helper = (GsHtmlUtilsHelper *) user_data;
+ g_autofree gchar *tmp = NULL;
+ g_auto(GStrv) split = NULL;
+ guint i;
+ gchar *strip;
+
+ if (helper->action == GS_HTML_UTILS_ACTION_IGNORE)
+ return;
+
+ /* only add valid lines */
+ tmp = g_markup_escape_text (text, text_len);
+ split = g_strsplit (tmp, "\n", -1);
+ for (i = 0; split[i] != NULL; i++) {
+ strip = g_strstrip (split[i]);
+ if (strip[0] == '\0')
+ continue;
+ if (strlen (strip) < 15)
+ continue;
+ switch (helper->action) {
+ case GS_HTML_UTILS_ACTION_PARA:
+ g_string_append_printf (helper->str, "<p>%s</p>", strip);
+ break;
+ case GS_HTML_UTILS_ACTION_LI:
+ g_string_append_printf (helper->str, "<li>%s</li>", strip);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+/**
+ * gs_html_utils_strreplace:
+ **/
+static void
+gs_html_utils_strreplace (GString *str, const gchar *search, const gchar *replace)
+{
+ g_auto(GStrv) split = NULL;
+ g_autofree gchar *new = NULL;
+
+ /* optimise */
+ if (g_strstr_len (str->str, -1, search) == NULL)
+ return;
+ split = g_strsplit (str->str, search, -1);
+ new = g_strjoinv (replace, split);
+ g_string_assign (str, new);
+}
+
+/**
+ * gs_html_utils_erase:
+ *
+ * Replaces any tag with whitespace.
+ **/
+static void
+gs_html_utils_erase (GString *str, const gchar *start, const gchar *end)
+{
+ guint i, j;
+ guint start_len = strlen (start);
+ guint end_len = strlen (end);
+ for (i = 0; str->str[i] != '\0'; i++) {
+ if (memcmp (&str->str[i], start, start_len) != 0)
+ continue;
+ for (j = i; i < str->len; j++) {
+ if (memcmp (&str->str[j], end, end_len) != 0)
+ continue;
+ /* delete this section and restart the search */
+ g_string_erase (str, i, (j - i) + end_len);
+ i = -1;
+ break;
+ }
+ }
+}
+
+/**
+ * gs_html_utils_parse_description:
+ **/
+gchar *
+gs_html_utils_parse_description (const gchar *html, GError **error)
+{
+ GMarkupParseContext *ctx;
+ GsHtmlUtilsHelper helper;
+ GMarkupParser parser = {
+ gs_html_utils_start_cb,
+ gs_html_utils_end_cb,
+ gs_html_utils_text_cb,
+ NULL,
+ NULL };
+ g_autofree gchar *tmp = NULL;
+ g_autoptr(GString) str = NULL;
+
+ /* set up XML parser */
+ helper.action = GS_HTML_UTILS_ACTION_PARA;
+ helper.str = g_string_new ("");
+ ctx = g_markup_parse_context_new (&parser, G_MARKUP_TREAT_CDATA_AS_TEXT, &helper, NULL);
+
+ /* ensure this has at least one se of quotes */
+ str = g_string_new ("");
+ g_string_append_printf (str, "<book>%s</book>", html);
+
+ /* convert win32 line endings */
+ g_strdelimit (str->str, "\r", '\n');
+
+ /* tidy up non-compliant HTML5 */
+ gs_html_utils_erase (str, "<img", ">");
+ gs_html_utils_erase (str, "<br", ">");
+
+ /* kill anything that's not wanted */
+ gs_html_utils_erase (str, "<h1", "</h1>");
+ gs_html_utils_erase (str, "<h2", "</h2>");
+ gs_html_utils_erase (str, "<span", "</span>");
+ gs_html_utils_erase (str, "<a", ">");
+ gs_html_utils_erase (str, "</a", ">");
+
+ /* use UTF-8 */
+ gs_html_utils_strreplace (str, "<i>", "");
+ gs_html_utils_strreplace (str, "</i>", "");
+ gs_html_utils_strreplace (str, "<u>", "");
+ gs_html_utils_strreplace (str, "</u>", "");
+ gs_html_utils_strreplace (str, "<b>", "");
+ gs_html_utils_strreplace (str, "</b>", "");
+ gs_html_utils_strreplace (str, "<blockquote>", "");
+ gs_html_utils_strreplace (str, "</blockquote>", "");
+ gs_html_utils_strreplace (str, "<strong>", "");
+ gs_html_utils_strreplace (str, "</strong>", "");
+ gs_html_utils_strreplace (str, "™", "™");
+ gs_html_utils_strreplace (str, "®", "®");
+
+//g_print ("%s\n", str->str);
+
+ /* parse */
+ if (!g_markup_parse_context_parse (ctx, str->str, -1, error))
+ return NULL;
+
+ /* return only valid AppStream markup */
+ return as_markup_convert_full (helper.str->str,
+ AS_MARKUP_CONVERT_FORMAT_APPSTREAM,
+ AS_MARKUP_CONVERT_FLAG_IGNORE_ERRORS,
+ error);
+}
diff --git a/src/plugins/gs-html-utils.h b/src/plugins/gs-html-utils.h
new file mode 100644
index 0000000..35cc262
--- /dev/null
+++ b/src/plugins/gs-html-utils.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2014 Richard Hughes <richard hughsie com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __GS_STEAM_DESCRIPTION_H
+#define __GS_STEAM_DESCRIPTION_H
+
+#include <glib-object.h>
+
+gchar *gs_html_utils_parse_description (const gchar *html,
+ GError **error);
+
+
+G_END_DECLS
+
+#endif /* __GS_STEAM_DESCRIPTION_H */
+
diff --git a/src/plugins/gs-plugin-steam.c b/src/plugins/gs-plugin-steam.c
new file mode 100644
index 0000000..45e8477
--- /dev/null
+++ b/src/plugins/gs-plugin-steam.c
@@ -0,0 +1,1079 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015-2016 Richard Hughes <richard hughsie com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <config.h>
+
+#include <gs-plugin.h>
+#include <string.h>
+#include <icns.h>
+
+#include "gs-html-utils.h"
+#include "gs-utils.h"
+
+/**
+ * gs_plugin_get_name:
+ */
+const gchar *
+gs_plugin_get_name (void)
+{
+ return "steam";
+}
+
+/**
+ * gs_plugin_steam_html_download:
+ */
+static gboolean
+gs_plugin_steam_html_download (GsPlugin *plugin,
+ const gchar *uri,
+ gchar **data,
+ gsize *data_len,
+ GError **error)
+{
+ guint status_code;
+ g_autoptr(GInputStream) stream = NULL;
+ g_autoptr(SoupMessage) msg = NULL;
+
+ /* create the GET data */
+ msg = soup_message_new (SOUP_METHOD_GET, uri);
+ if (msg == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "%s is not a valid URL", uri);
+ return FALSE;
+ }
+
+ /* set sync request */
+ status_code = soup_session_send_message (plugin->soup_session, msg);
+ if (status_code != SOUP_STATUS_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Failed to download icon %s: %s",
+ uri, soup_status_get_phrase (status_code));
+ return FALSE;
+ }
+
+ /* return data */
+ if (data != NULL)
+ *data = g_memdup (msg->response_body->data,
+ msg->response_body->length);
+ if (data_len != NULL)
+ *data_len = msg->response_body->length;
+ return TRUE;
+}
+
+/**
+ * gs_plugin_get_deps:
+ */
+const gchar **
+gs_plugin_get_deps (GsPlugin *plugin)
+{
+ static const gchar *deps[] = {
+ "appstream", /* need metadata */
+ NULL };
+ return deps;
+}
+
+typedef enum {
+ GS_PLUGIN_STEAM_TOKEN_START = 0x00,
+ GS_PLUGIN_STEAM_TOKEN_STRING = 0x01,
+ GS_PLUGIN_STEAM_TOKEN_INTEGER = 0x02,
+ GS_PLUGIN_STEAM_TOKEN_END = 0x08,
+ GS_PLUGIN_STEAM_TOKEN_LAST,
+} GsPluginSteamToken;
+
+/**
+ * gs_plugin_steam_token_kind_to_str:
+ **/
+static const gchar *
+gs_plugin_steam_token_kind_to_str (guint8 data)
+{
+ static gchar tmp[2] = { 0x00, 0x00 };
+
+ if (data == GS_PLUGIN_STEAM_TOKEN_START)
+ return "[SRT]";
+ if (data == GS_PLUGIN_STEAM_TOKEN_STRING)
+ return "[STR]";
+ if (data == GS_PLUGIN_STEAM_TOKEN_INTEGER)
+ return "[INT]";
+ if (data == GS_PLUGIN_STEAM_TOKEN_END)
+ return "[END]";
+
+ /* guess */
+ if (data == 0x03)
+ return "[ETX]";
+ if (data == 0x04)
+ return "[EOT]";
+ if (data == 0x05)
+ return "[ENQ]";
+ if (data == 0x06)
+ return "[ACK]";
+ if (data == 0x07)
+ return "[BEL]";
+ if (data == 0x09)
+ return "[SMI]";
+
+ /* printable */
+ if (g_ascii_isprint (data)) {
+ tmp[0] = data;
+ return tmp;
+ }
+ return "[?]";
+}
+
+/**
+ * gs_plugin_steam_consume_uint32:
+ **/
+static guint32
+gs_plugin_steam_consume_uint32 (guint8 *data, gsize data_len, guint *idx)
+{
+ guint32 tmp = *((guint32 *) &data[*idx + 1]);
+ *idx += 4;
+ return tmp;
+}
+
+/**
+ * gs_plugin_steam_consume_string:
+ **/
+static const gchar *
+gs_plugin_steam_consume_string (guint8 *data, gsize data_len, guint *idx)
+{
+ const gchar *tmp;
+
+ /* this may be an empty string */
+ tmp = (const gchar *) &data[*idx+1];
+ if (tmp[0] == '\0') {
+ (*idx)++;
+ return NULL;
+ }
+ *idx += strlen (tmp) + 1;
+ return tmp;
+}
+
+/**
+ * gs_plugin_steam_find_next_sync_point:
+ **/
+static void
+gs_plugin_steam_find_next_sync_point (guint8 *data, gsize data_len, guint *idx)
+{
+ guint i;
+ for (i = *idx; i < data_len; i++) {
+ if (memcmp (&data[i], "\0\x02\0common\0", 8) == 0) {
+ *idx = i - 1;
+ return;
+ }
+ }
+ *idx = 0xfffffffe;
+}
+
+/**
+ * gs_plugin_steam_add_app:
+ **/
+static GHashTable *
+gs_plugin_steam_add_app (GPtrArray *apps)
+{
+ GHashTable *app;
+ app = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, (GDestroyNotify) g_variant_unref);
+ g_ptr_array_add (apps, app);
+ return app;
+}
+
+/**
+ * gs_plugin_steam_parse_appinfo_file:
+ **/
+static GPtrArray *
+gs_plugin_steam_parse_appinfo_file (const gchar *filename, GError **error)
+{
+ GPtrArray *apps;
+ GHashTable *app = NULL;
+ const gchar *tmp;
+ guint8 *data = NULL;
+ gsize data_len = 0;
+ guint i = 0;
+ gboolean debug = g_getenv ("GS_PLUGIN_STEAM_DEBUG") != NULL;
+
+ /* load file */
+ if (!g_file_get_contents (filename, (gchar **) &data, &data_len, error))
+ return NULL;
+
+ /* a GPtrArray of GHashTable */
+ apps = g_ptr_array_new_with_free_func ((GDestroyNotify) g_hash_table_unref);
+
+ /* find the first application and avoid header */
+ gs_plugin_steam_find_next_sync_point (data, data_len, &i);
+ for (i = i + 1; i < data_len; i++) {
+ if (debug)
+ g_debug ("%04x {0x%02x} %s", i, data[i], gs_plugin_steam_token_kind_to_str (data[i]));
+ if (data[i] == GS_PLUGIN_STEAM_TOKEN_START) {
+
+ /* this is a new application/game */
+ if (data[i+1] == 0x02) {
+ /* reset */
+ app = gs_plugin_steam_add_app (apps);
+ i++;
+ continue;
+ }
+
+ /* new group */
+ if (g_ascii_isprint (data[i+1])) {
+ tmp = gs_plugin_steam_consume_string (data, data_len, &i);
+ if (debug)
+ g_debug ("[%s] {", tmp);
+ continue;
+ }
+
+ /* something went wrong */
+ if (debug)
+ g_debug ("CORRUPTION DETECTED");
+ gs_plugin_steam_find_next_sync_point (data, data_len, &i);
+ continue;
+ }
+ if (data[i] == GS_PLUGIN_STEAM_TOKEN_END) {
+ if (debug)
+ g_debug ("}");
+ continue;
+ }
+ if (data[i] == GS_PLUGIN_STEAM_TOKEN_STRING) {
+ const gchar *value;
+ tmp = gs_plugin_steam_consume_string (data, data_len, &i);
+ value = gs_plugin_steam_consume_string (data, data_len, &i);
+ if (debug)
+ g_debug ("\t%s=%s", tmp, value);
+ if (tmp != NULL && value != NULL) {
+ if (g_hash_table_lookup (app, tmp) != NULL)
+ continue;
+ g_hash_table_insert (app,
+ g_strdup (tmp),
+ g_variant_new_string (value));
+ }
+ continue;
+ }
+ if (data[i] == GS_PLUGIN_STEAM_TOKEN_INTEGER) {
+ guint32 value;
+ tmp = gs_plugin_steam_consume_string (data, data_len, &i);
+ value = gs_plugin_steam_consume_uint32 (data, data_len, &i);
+ if (debug)
+ g_debug ("\t%s=%i", tmp, value);
+ if (tmp != NULL) {
+ if (g_hash_table_lookup (app, tmp) != NULL)
+ continue;
+ g_hash_table_insert (app,
+ g_strdup (tmp),
+ g_variant_new_uint32 (value));
+ }
+ continue;
+ }
+ }
+
+ return apps;
+}
+
+/**
+ * gs_plugin_steam_dump_apps:
+ **/
+static void
+gs_plugin_steam_dump_apps (GPtrArray *apps)
+{
+ guint i;
+ GHashTable *app;
+
+ for (i = 0; i < apps->len; i++) {
+ g_autoptr(GList) keys = NULL;
+ GList *l;
+ app = g_ptr_array_index (apps, i);
+ keys = g_hash_table_get_keys (app);
+ for (l = keys; l != NULL; l = l->next) {
+ const gchar *tmp;
+ GVariant *value;
+ tmp = l->data;
+ value = g_hash_table_lookup (app, tmp);
+ if (g_strcmp0 (g_variant_get_type_string (value), "s") == 0)
+ g_print ("%s=%s\n", tmp, g_variant_get_string (value, NULL));
+ else if (g_strcmp0 (g_variant_get_type_string (value), "u") == 0)
+ g_print ("%s=%u\n", tmp, g_variant_get_uint32 (value));
+ }
+ g_print ("\n");
+ }
+}
+
+/**
+ * gs_plugin_steam_capture:
+ *
+ * Returns: A string between @start and @end, or %NULL
+ **/
+static gchar *
+gs_plugin_steam_capture (const gchar *html,
+ const gchar *start,
+ const gchar *end,
+ guint *offset)
+{
+ guint i;
+ guint j;
+ guint start_len;
+ guint end_len;
+
+ /* find @start */
+ start_len = strlen (start);
+ for (i = *offset; html[i] != '\0'; i++) {
+ if (memcmp (&html[i], start, start_len) != 0)
+ continue;
+ /* find @end */
+ end_len = strlen (end);
+ for (j = i + start_len; html[j] != '\0'; j++) {
+ if (memcmp (&html[j], end, end_len) != 0)
+ continue;
+ *offset = j + end_len;
+ return g_strndup (&html[i + start_len],
+ j - i - start_len);
+ }
+ }
+ return NULL;
+}
+
+/**
+ * gs_plugin_steam_update_screenshots:
+ **/
+static gboolean
+gs_plugin_steam_update_screenshots (AsApp *app, const gchar *html, GError **error)
+{
+ const gchar *gameid_str;
+ gchar *tmp1;
+ guint i = 0;
+ guint idx = 0;
+
+ /* find all the screenshots */
+ gameid_str = as_app_get_metadata_item (app, "X-Steam-GameID");
+ while ((tmp1 = gs_plugin_steam_capture (html, "data-screenshotid=\"", "\"", &i))) {
+ g_autoptr(AsImage) im = NULL;
+ g_autoptr(AsScreenshot) ss = NULL;
+ g_autofree gchar *cdn_uri = NULL;
+
+ /* create an image */
+ im = as_image_new ();
+ as_image_set_kind (im, AS_IMAGE_KIND_SOURCE);
+ cdn_uri = g_strdup_printf ("http://cdn.akamai.steamstatic.com/steam/apps/%s/%s", gameid_str,
tmp1);
+ as_image_set_url (im, cdn_uri);
+
+ /* create screenshot with no caption */
+ ss = as_screenshot_new ();
+ as_screenshot_set_kind (ss, idx == 0 ? AS_SCREENSHOT_KIND_DEFAULT :
+ AS_SCREENSHOT_KIND_NORMAL);
+ as_screenshot_add_image (ss, im);
+ as_app_add_screenshot (app, ss);
+ g_free (tmp1);
+
+ /* limit this to a sane number */
+ if (idx++ >= 4)
+ break;
+ }
+ return TRUE;
+}
+
+/**
+ * gs_plugin_steam_update_description:
+ **/
+static gboolean
+gs_plugin_steam_update_description (AsApp *app, const gchar *html, GError **error)
+{
+ guint i = 0;
+ g_autofree gchar *desc = NULL;
+ g_autofree gchar *subsect = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ /* get the game description div section */
+ subsect = gs_plugin_steam_capture (html,
+ "<div id=\"game_area_description\" class=\"game_area_description\">",
+ "</div>", &i);
+
+ /* fall back gracefully */
+ if (subsect == NULL) {
+ subsect = gs_plugin_steam_capture (html,
+ "<meta name=\"Description\" content=\"",
+ "\">", &i);
+ }
+ if (subsect == NULL) {
+ g_warning ("Failed to get description for %s [%s]",
+ as_app_get_name (app, NULL),
+ as_app_get_id (app));
+ return TRUE;
+ }
+ desc = gs_html_utils_parse_description (subsect, &error_local);
+ if (desc == NULL) {
+ g_warning ("Failed to parse description for %s [%s]: %s",
+ as_app_get_name (app, NULL),
+ as_app_get_id (app),
+ error_local->message);
+ return TRUE;
+ }
+ as_app_set_description (app, NULL, desc);
+ return TRUE;
+}
+
+/**
+ * gs_plugin_steam_new_pixbuf_from_icns:
+ **/
+static GdkPixbuf *
+gs_plugin_steam_new_pixbuf_from_icns (const gchar *fn, GError **error)
+{
+ GdkPixbuf *pb = NULL;
+ FILE *datafile;
+ guint i;
+ icns_family_t *icon_family = NULL;
+ icns_image_t im;
+ int rc;
+ icns_type_t preference[] = {
+ ICNS_128X128_32BIT_DATA,
+ ICNS_256x256_32BIT_ARGB_DATA,
+ ICNS_48x48_32BIT_DATA,
+ 0 };
+
+ /* open file */
+ datafile = fopen (fn, "rb");
+ rc = icns_read_family_from_file (datafile, &icon_family);
+ if (rc != 0) {
+ g_set_error (error, 1, 0, "Failed to read icon %s", fn);
+ return NULL;
+ }
+
+ /* libicns 'helpfully' frees the @arg */
+ im.imageData = NULL;
+
+ /* get the best sized icon */
+ for (i = 0; preference[i] != 0; i++) {
+ rc = icns_get_image32_with_mask_from_family (icon_family,
+ preference[i],
+ &im);
+ if (rc == 0) {
+ gchar buf[5];
+ icns_type_str (preference[i], buf);
+ g_debug ("using ICNS %s for %s", buf, fn);
+ break;
+ }
+ }
+ if (im.imageData == NULL) {
+ g_set_error (error, 1, 0, "Failed to get icon %s", fn);
+ return NULL;
+ }
+
+ /* create the pixbuf */
+ pb = gdk_pixbuf_new_from_data (im.imageData,
+ GDK_COLORSPACE_RGB,
+ TRUE,
+ im.imagePixelDepth,
+ im.imageWidth,
+ im.imageHeight,
+ im.imageWidth * im.imageChannels,
+ NULL, //??
+ NULL);
+ g_assert (pb != NULL);
+
+ fclose (datafile);
+ return pb;
+}
+
+/**
+ * gs_plugin_steam_download_icon:
+ **/
+static gboolean
+gs_plugin_steam_download_icon (GsPlugin *plugin, AsApp *app, const gchar *uri, GError **error)
+{
+ const gchar *gameid_str;
+ gsize data_len;
+ g_autofree gchar *cache_basename = NULL;
+ g_autofree gchar *cache_fn = NULL;
+ g_autofree gchar *cache_png = NULL;
+ g_autofree gchar *data = NULL;
+ g_autoptr(AsIcon) icon = NULL;
+ g_autoptr(GdkPixbuf) pb = NULL;
+
+ /* download icons from the cdn */
+ gameid_str = as_app_get_metadata_item (app, "X-Steam-GameID");
+ cache_basename = g_path_get_basename (uri);
+ cache_fn = g_build_filename (g_get_user_cache_dir (),
+ "gnome-software",
+ "steam",
+ cache_basename,
+ NULL);
+ if (g_file_test (cache_fn, G_FILE_TEST_EXISTS)) {
+ if (!g_file_get_contents (cache_fn, &data, &data_len, error))
+ return FALSE;
+ } else {
+ if (!gs_plugin_steam_html_download (plugin, uri, &data, &data_len, error))
+ return FALSE;
+ if (!gs_mkdir_parent (cache_fn, error))
+ return FALSE;
+ if (!g_file_set_contents (cache_fn, data, data_len, error))
+ return FALSE;
+ }
+
+ /* check the icns file is not just a png/ico/jpg file in disguise */
+ if (memcmp (data + 1, "\x89PNG", 4) == 0 ||
+ memcmp (data, "\x00\x00\x01\x00", 4) == 0 ||
+ memcmp (data, "\xff\xd8\xff", 3) == 0) {
+ g_debug ("using fallback for %s\n", cache_fn);
+ pb = gdk_pixbuf_new_from_file (cache_fn, error);
+ if (pb == NULL)
+ return FALSE;
+ } else {
+ pb = gs_plugin_steam_new_pixbuf_from_icns (cache_fn, error);
+ if (pb == NULL)
+ return FALSE;
+ }
+
+ /* too small? */
+ if (gdk_pixbuf_get_width (pb) < 48 ||
+ gdk_pixbuf_get_height (pb) < 48) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "icon is too small %ix%i",
+ gdk_pixbuf_get_width (pb),
+ gdk_pixbuf_get_height (pb));
+ return FALSE;
+ }
+
+ /* save to cache */
+ memcpy (cache_basename + 40, ".png\0", 5);
+ cache_png = g_build_filename (g_get_user_cache_dir (),
+ "gnome-software",
+ "steam",
+ cache_basename,
+ NULL);
+ if (!gdk_pixbuf_save (pb, cache_png, "png", error, NULL))
+ return FALSE;
+
+ /* add an icon */
+ icon = as_icon_new ();
+ as_icon_set_kind (icon, AS_ICON_KIND_LOCAL);
+ as_icon_set_filename (icon, cache_png);
+ as_app_add_icon (app, icon);
+ return TRUE;
+}
+
+/**
+ * gs_plugin_steam_update_store_app:
+ **/
+static gboolean
+gs_plugin_steam_update_store_app (GsPlugin *plugin,
+ AsStore *store,
+ GHashTable *app,
+ GError **error)
+{
+ const gchar *name;
+ GVariant *tmp;
+ guint32 gameid;
+ gchar *app_id;
+ g_autofree gchar *cache_basename = NULL;
+ g_autofree gchar *cache_fn = NULL;
+ g_autofree gchar *gameid_str = NULL;
+ g_autofree gchar *html = NULL;
+ g_autofree gchar *uri = NULL;
+ g_autoptr(AsApp) item = NULL;
+
+ /* this is the key */
+ tmp = g_hash_table_lookup (app, "gameid");
+ if (tmp == NULL)
+ return TRUE;
+ gameid = g_variant_get_uint32 (tmp);
+
+ /* valve use the name as the application ID, not the gameid */
+ tmp = g_hash_table_lookup (app, "name");
+ if (tmp == NULL)
+ return TRUE;
+ name = g_variant_get_string (tmp, NULL);
+ app_id = g_strdup_printf ("%s.desktop", name);
+
+ /* already exists */
+ if (as_store_get_app_by_id (store, app_id) != NULL) {
+ g_debug ("already exists %i, skipping", gameid);
+ return TRUE;
+ }
+
+ /* create application with the gameid as the key */
+ g_debug ("parsing steam %i", gameid);
+ item = as_app_new ();
+ as_app_set_project_license (item, "Steam");
+ as_app_set_id (item, app_id);
+ as_app_set_name (item, NULL, name);
+ as_app_add_category (item, "Game");
+ as_app_add_kudo_kind (item, AS_KUDO_KIND_MODERN_TOOLKIT);
+ as_app_set_comment (item, NULL, "Available on Steam");
+
+ /* this is for the GNOME Software plugin */
+ gameid_str = g_strdup_printf ("%" G_GUINT32_FORMAT, gameid);
+ as_app_add_metadata (item, "X-Steam-GameID", gameid_str);
+
+ /* ban certains apps based on the name */
+ if (g_strstr_len (name, -1, "Dedicated Server") != NULL)
+ as_app_add_veto (item, "Dedicated Server");
+
+ /* oslist */
+ tmp = g_hash_table_lookup (app, "oslist");
+ if (tmp == NULL) {
+ as_app_add_veto (item, "No operating systems listed");
+ } else if (g_strstr_len (g_variant_get_string (tmp, NULL), -1, "linux") == NULL) {
+ as_app_add_veto (item, "No Linux support");
+ }
+
+ /* url: homepage */
+ tmp = g_hash_table_lookup (app, "homepage");
+ if (tmp != NULL)
+ as_app_add_url (item, AS_URL_KIND_HOMEPAGE, g_variant_get_string (tmp, NULL));
+
+ /* developer name */
+ tmp = g_hash_table_lookup (app, "developer");
+ if (tmp != NULL)
+ as_app_set_developer_name (item, NULL, g_variant_get_string (tmp, NULL));
+
+ /* type */
+ tmp = g_hash_table_lookup (app, "type");
+ if (tmp != NULL) {
+ const gchar *kind = g_variant_get_string (tmp, NULL);
+ if (g_strcmp0 (kind, "DLC") == 0 ||
+ g_strcmp0 (kind, "Config") == 0 ||
+ g_strcmp0 (kind, "Tool") == 0)
+ as_app_add_veto (item, "type is %s", kind);
+ }
+
+ /* don't bother saving apps with failures */
+ if (as_app_get_vetos(item)->len > 0)
+ return TRUE;
+
+ /* icons */
+ tmp = g_hash_table_lookup (app, "clienticns");
+ if (tmp != NULL) {
+ g_autoptr(GError) error_local = NULL;
+ g_autofree gchar *ic_uri = NULL;
+ ic_uri = g_strdup_printf
("https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/%i/%s.icns",
+ gameid, g_variant_get_string (tmp, NULL));
+ if (!gs_plugin_steam_download_icon (plugin, item, ic_uri, &error_local)) {
+ g_warning ("Failed to parse clienticns: %s",
+ error_local->message);
+ }
+ }
+
+ /* try clienticon */
+ if (as_app_get_icons(item)->len == 0) {
+ tmp = g_hash_table_lookup (app, "clienticon");
+ if (tmp != NULL) {
+ g_autoptr(GError) error_local = NULL;
+ g_autofree gchar *ic_uri = NULL;
+ ic_uri = g_strdup_printf
("http://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/%i/%s.ico",
+ gameid, g_variant_get_string (tmp, NULL));
+ if (!gs_plugin_steam_download_icon (plugin, item, ic_uri, &error_local)) {
+ g_warning ("Failed to parse clienticon: %s",
+ error_local->message);
+ }
+ }
+ }
+
+ /* fall back to a resized logo */
+ if (as_app_get_icons(item)->len == 0) {
+ tmp = g_hash_table_lookup (app, "logo");
+ if (tmp != NULL) {
+ AsIcon *icon = NULL;
+ g_autofree gchar *ic_uri = NULL;
+ ic_uri = g_strdup_printf
("http://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/%i/%s.jpg",
+ gameid, g_variant_get_string (tmp, NULL));
+ icon = as_icon_new ();
+ as_icon_set_kind (icon, AS_ICON_KIND_REMOTE);
+ as_icon_set_url (icon, ic_uri);
+ as_app_add_icon (item, icon);
+ }
+ }
+
+ /* size */
+ tmp = g_hash_table_lookup (app, "maxsize");
+ if (tmp != NULL) {
+ /* string when over 16Gb... :/ */
+ if (g_strcmp0 (g_variant_get_type_string (tmp), "u") == 0) {
+ g_autofree gchar *val = NULL;
+ val = g_strdup_printf ("%" G_GUINT32_FORMAT,
+ g_variant_get_uint32 (tmp));
+ as_app_add_metadata (item, "X-Steam-Size", val);
+ } else {
+ as_app_add_metadata (item, "X-Steam-Size",
+ g_variant_get_string (tmp, NULL));
+ }
+ }
+
+ /* download page from the store */
+ cache_basename = g_strdup_printf ("%s.html", gameid_str);
+ cache_fn = g_build_filename (g_get_user_cache_dir (),
+ "gnome-software",
+ "steam",
+ cache_basename,
+ NULL);
+ if (g_file_test (cache_fn, G_FILE_TEST_EXISTS)) {
+ if (!g_file_get_contents (cache_fn, &html, NULL, error))
+ return FALSE;
+ } else {
+ uri = g_strdup_printf ("http://store.steampowered.com/app/%s/", gameid_str);
+ if (!gs_plugin_steam_html_download (plugin, uri, &html, NULL, error))
+ return FALSE;
+ if (!gs_mkdir_parent (cache_fn, error))
+ return FALSE;
+ if (!g_file_set_contents (cache_fn, html, -1, error))
+ return FALSE;
+ }
+
+ /* get screenshots and descriptions */
+ if (!gs_plugin_steam_update_screenshots (item, html, error))
+ return FALSE;
+ if (!gs_plugin_steam_update_description (item, html, error))
+ return FALSE;
+
+ /* add */
+ as_store_add_app (store, item);
+ return TRUE;
+}
+
+/**
+ * gs_plugin_steam_update_store:
+ */
+static gboolean
+gs_plugin_steam_update_store (GsPlugin *plugin, AsStore *store, GPtrArray *apps, GError **error)
+{
+ guint i;
+ GHashTable *app;
+
+ for (i = 0; i < apps->len; i++) {
+ app = g_ptr_array_index (apps, i);
+ if (!gs_plugin_steam_update_store_app (plugin, store, app, error))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/**
+ * gs_plugin_refresh:
+ */
+gboolean
+gs_plugin_refresh (GsPlugin *plugin,
+ guint cache_age,
+ GsPluginRefreshFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(AsStore) store = NULL;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GPtrArray) apps = NULL;
+ g_autofree gchar *fn = NULL;
+ g_autofree gchar *fn_xml = NULL;
+
+ /* check if exists */
+ fn = g_build_filename (g_get_user_data_dir (),
+ "Steam", "appcache", "appinfo.vdf", NULL);
+ if (!g_file_test (fn, G_FILE_TEST_EXISTS)) {
+ g_debug ("no %s, so skipping", fn);
+ return TRUE;
+ }
+
+ /* test cache age */
+ fn_xml = g_build_filename (g_get_user_data_dir (),
+ "app-info", "xmls", "steam.xml.gz", NULL);
+ file = g_file_new_for_path (fn_xml);
+ if (cache_age > 0) {
+ guint tmp;
+ tmp = gs_utils_get_file_age (file);
+ if (tmp < cache_age) {
+ g_debug ("%s is only %i seconds old, so ignoring refresh",
+ fn_xml, tmp);
+ return TRUE;
+ }
+ }
+
+ /* parse it */
+ apps = gs_plugin_steam_parse_appinfo_file (fn, error);
+ if (apps == NULL)
+ return FALSE;
+
+ /* debug */
+ if (g_getenv ("GS_PLUGIN_STEAM_DEBUG") != NULL)
+ gs_plugin_steam_dump_apps (apps);
+
+ /* load existing AppStream XML */
+ store = as_store_new ();
+ if (g_file_query_exists (file, cancellable)) {
+ if (!as_store_from_file (store, file, NULL, cancellable, error))
+ return FALSE;
+ }
+
+ /* update any new applications */
+ if (!gs_plugin_steam_update_store (plugin, store, apps, error))
+ return FALSE;
+
+ /* save new file */
+ return as_store_to_file (store, file,
+ AS_NODE_TO_XML_FLAG_FORMAT_INDENT |
+ AS_NODE_TO_XML_FLAG_FORMAT_MULTILINE,
+ NULL,
+ error);
+}
+
+/**
+ * gs_plugin_steam_load_app_manifest:
+ */
+static GHashTable *
+gs_plugin_steam_load_app_manifest (const gchar *fn, GError **error)
+{
+ GHashTable *manifest = NULL;
+ guint i;
+ guint j;
+ g_autofree gchar *data = NULL;
+ g_auto(GStrv) lines = NULL;
+
+ /* get file */
+ if (!g_file_get_contents (fn, &data, NULL, error))
+ return NULL;
+
+ /* parse each line */
+ manifest = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+ lines = g_strsplit (data, "\n", -1);
+ for (i = 0; lines[i] != NULL; i++) {
+ gboolean is_key = TRUE;
+ const gchar *tmp = lines[i];
+ g_autoptr(GString) key = g_string_new ("");
+ g_autoptr(GString) value = g_string_new ("");
+ for (j = 0; tmp[j] != '\0'; j++) {
+
+ /* alphanum, so either key or value */
+ if (g_ascii_isalnum (tmp[j])) {
+ g_string_append_c (is_key ? key : value, tmp[j]);
+ continue;
+ }
+
+ /* first whitespace after the key */
+ if (g_ascii_isspace (tmp[j]) && key->len > 0)
+ is_key = FALSE;
+ }
+ if (g_getenv ("GS_PLUGIN_STEAM_DEBUG") != NULL)
+ g_debug ("manifest %s=%s", key->str, value->str);
+ if (key->len == 0 || value->len == 0)
+ continue;
+ g_hash_table_insert (manifest, g_strdup (key->str), g_strdup (value->str));
+ }
+ return manifest;
+}
+
+typedef enum {
+ GS_STEAM_STATE_FLAG_INVALID = 0,
+ GS_STEAM_STATE_FLAG_UNINSTALLED = 1 << 0,
+ GS_STEAM_STATE_FLAG_UPDATE_REQUIRED = 1 << 1,
+ GS_STEAM_STATE_FLAG_FULLY_INSTALLED = 1 << 2,
+ GS_STEAM_STATE_FLAG_ENCRYPTED = 1 << 3,
+ GS_STEAM_STATE_FLAG_LOCKED = 1 << 4,
+ GS_STEAM_STATE_FLAG_FILES_MISSING = 1 << 5,
+ GS_STEAM_STATE_FLAG_APP_RUNNING = 1 << 6,
+ GS_STEAM_STATE_FLAG_FILES_CORRUPT = 1 << 7,
+ GS_STEAM_STATE_FLAG_UPDATE_RUNNING = 1 << 8,
+ GS_STEAM_STATE_FLAG_UPDATE_PAUSED = 1 << 9,
+ GS_STEAM_STATE_FLAG_UPDATE_STARTED = 1 << 10,
+ GS_STEAM_STATE_FLAG_UNINSTALLING = 1 << 11,
+ GS_STEAM_STATE_FLAG_BACKUP_RUNNING = 1 << 12,
+ /* not sure what happened here... */
+ GS_STEAM_STATE_FLAG_RECONFIGURING = 1 << 16,
+ GS_STEAM_STATE_FLAG_VALIDATING = 1 << 17,
+ GS_STEAM_STATE_FLAG_ADDING_FILES = 1 << 18,
+ GS_STEAM_STATE_FLAG_PREALLOCATING = 1 << 19,
+ GS_STEAM_STATE_FLAG_DOWNLOADING = 1 << 20,
+ GS_STEAM_STATE_FLAG_STAGING = 1 << 21,
+ GS_STEAM_STATE_FLAG_COMMITTING = 1 << 22,
+ GS_STEAM_STATE_FLAG_UPDATE_STOPPING = 1 << 23,
+ GS_STEAM_STATE_FLAG_LAST
+} GsSteamStateFlags;
+
+/**
+ * gs_plugin_steam_refine_app:
+ */
+static gboolean
+gs_plugin_steam_refine_app (GsPlugin *plugin,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *gameid;
+ const gchar *tmp;
+ g_autofree gchar *manifest_basename = NULL;
+ g_autofree gchar *fn = NULL;
+ g_autoptr(GHashTable) manifest = NULL;
+
+ /* check is us */
+ gameid = gs_app_get_metadata_item (app, "X-Steam-GameID");
+ if (gameid == NULL)
+ return TRUE;
+
+ /* is this true? */
+ gs_app_set_kind (app, AS_ID_KIND_DESKTOP);
+
+ /* size */
+ tmp = gs_app_get_metadata_item (app, "X-Steam-Size");
+ if (tmp != NULL) {
+ guint64 sz;
+ sz = g_ascii_strtoull (tmp, NULL, 10);
+ if (sz > 0)
+ gs_app_set_size (app, sz);
+ }
+
+ /* check manifest */
+ manifest_basename = g_strdup_printf ("appmanifest_%s.acf", gameid);
+ fn = g_build_filename (g_get_user_data_dir (),
+ "Steam",
+ "steamapps",
+ manifest_basename,
+ NULL);
+ if (!g_file_test (fn, G_FILE_TEST_EXISTS)) {
+ /* can never have been installed */
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+ return TRUE;
+ }
+ manifest = gs_plugin_steam_load_app_manifest (fn, error);
+ if (manifest == NULL)
+ return FALSE;
+
+ /* this is better than the download size */
+ tmp = g_hash_table_lookup (manifest, "SizeOnDisk");
+ if (tmp != NULL) {
+ guint64 sz;
+ sz = g_ascii_strtoull (tmp, NULL, 10);
+ if (sz > 0)
+ gs_app_set_size (app, sz);
+ }
+
+ /* set state */
+ tmp = g_hash_table_lookup (manifest, "StateFlags");
+ if (tmp != NULL) {
+ guint64 state_flags;
+
+ /* set state */
+ state_flags = g_ascii_strtoull (tmp, NULL, 10);
+ if (state_flags & GS_STEAM_STATE_FLAG_DOWNLOADING ||
+ state_flags & GS_STEAM_STATE_FLAG_PREALLOCATING ||
+ state_flags & GS_STEAM_STATE_FLAG_ADDING_FILES ||
+ state_flags & GS_STEAM_STATE_FLAG_COMMITTING ||
+ state_flags & GS_STEAM_STATE_FLAG_STAGING)
+ gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+ else if (state_flags & GS_STEAM_STATE_FLAG_UNINSTALLING)
+ gs_app_set_state (app, AS_APP_STATE_REMOVING);
+ else if (state_flags & GS_STEAM_STATE_FLAG_FULLY_INSTALLED)
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+ else if (state_flags & GS_STEAM_STATE_FLAG_UNINSTALLED)
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+ }
+
+ /* set install date */
+ tmp = g_hash_table_lookup (manifest, "LastUpdated");
+ if (tmp != NULL) {
+ guint64 ts;
+ ts = g_ascii_strtoull (tmp, NULL, 10);
+ if (ts > 0)
+ gs_app_set_install_date (app, ts);
+ }
+
+ return TRUE;
+}
+
+/**
+ * gs_plugin_refine:
+ */
+gboolean
+gs_plugin_refine (GsPlugin *plugin,
+ GList **list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GList *l;
+ GsApp *app;
+
+ for (l = *list; l != NULL; l = l->next) {
+ app = GS_APP (l->data);
+ if (!gs_plugin_steam_refine_app (plugin, app, flags,
+ cancellable, error))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/**
+ * gs_plugin_app_install:
+ */
+gboolean
+gs_plugin_app_install (GsPlugin *plugin, GsApp *app,
+ GCancellable *cancellable, GError **error)
+{
+ const gchar *gameid;
+ g_autofree gchar *cmdline = NULL;
+
+ /* check is us */
+ gameid = gs_app_get_metadata_item (app, "X-Steam-GameID");
+ if (gameid == NULL)
+ return TRUE;
+
+ /* this is async as steam is a different process: FIXME: use D-Bus */
+ gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+ cmdline = g_strdup_printf ("steam steam://install/%s", gameid);
+ return g_spawn_command_line_sync (cmdline, NULL, NULL, NULL, error);
+}
+
+/**
+ * gs_plugin_app_remove:
+ */
+gboolean
+gs_plugin_app_remove (GsPlugin *plugin, GsApp *app,
+ GCancellable *cancellable, GError **error)
+{
+ const gchar *gameid;
+ g_autofree gchar *cmdline = NULL;
+
+ /* check is us */
+ gameid = gs_app_get_metadata_item (app, "X-Steam-GameID");
+ if (gameid == NULL)
+ return TRUE;
+
+ /* this is async as steam is a different process: FIXME: use D-Bus */
+ gs_app_set_state (app, AS_APP_STATE_REMOVING);
+ cmdline = g_strdup_printf ("steam steam://uninstall/%s", gameid);
+ return g_spawn_command_line_sync (cmdline, NULL, NULL, NULL, error);
+}
+
+/**
+ * gs_plugin_launch:
+ */
+gboolean
+gs_plugin_launch (GsPlugin *plugin, GsApp *app,
+ GCancellable *cancellable, GError **error)
+{
+ const gchar *gameid;
+ g_autofree gchar *cmdline = NULL;
+
+ /* check is us */
+ gameid = gs_app_get_metadata_item (app, "X-Steam-GameID");
+ if (gameid == NULL)
+ return TRUE;
+
+ /* this is async as steam is a different process: FIXME: use D-Bus */
+ cmdline = g_strdup_printf ("steam steam://run/%s", gameid);
+ return g_spawn_command_line_sync (cmdline, NULL, NULL, NULL, error);
+}
diff --git a/src/plugins/gs-self-test.c b/src/plugins/gs-self-test.c
index 6b45897..c81fad4 100644
--- a/src/plugins/gs-self-test.c
+++ b/src/plugins/gs-self-test.c
@@ -27,6 +27,44 @@
#include <gtk/gtk.h>
#include "gs-moduleset.h"
+#include "gs-html-utils.h"
+
+static void
+html_utils_func (void)
+{
+ const gchar *input;
+ g_autofree gchar *out_complex = NULL;
+ g_autofree gchar *out_list = NULL;
+ g_autofree gchar *out_simple = NULL;
+ g_autoptr(GError) error = NULL;
+
+ /* simple, from meta */
+ input = "This game is simply awesome™ in every way!";
+ out_simple = gs_html_utils_parse_description (input, &error);
+ g_assert_no_error (error);
+ g_assert_cmpstr (out_simple, ==, "<p>This game is simply awesome™ in every way!</p>");
+
+ /* complex non-compliant HTML, from div */
+ input = " <h1>header</h1>"
+ " <p>First line of the <i>description</i> is okay...</p>"
+ " <img src=\"moo.png\">"
+ " <img src=\"png\">"
+ " <p>Second <strong>line</strong> is <a href=\"#moo\">even</a> better!</p>";
+ out_complex = gs_html_utils_parse_description (input, &error);
+ g_print ("\n\n%s\n\n", out_complex);
+ g_assert_no_error (error);
+ g_assert_cmpstr (out_complex, ==, "<p>First line of the description is okay...</p>"
+ "<p>Second line is even better!</p>");
+
+ /* complex list */
+ input = " <ul>"
+ " <li>First line of the list</li>"
+ " <li>Second line of the list</li>"
+ " </ul>";
+ out_list = gs_html_utils_parse_description (input, &error);
+ g_assert_no_error (error);
+ g_assert_cmpstr (out_list, ==, "<ul><li>First line of the list</li><li>Second line of the
list</li></ul>");
+}
static void
moduleset_func (void)
@@ -76,6 +114,7 @@ main (int argc, char **argv)
/* tests go here */
g_test_add_func ("/moduleset", moduleset_func);
+ g_test_add_func ("/html-utils", html_utils_func);
return g_test_run ();
}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]