diff --git a/data/com.github.tkashkin.gamehub.gschema.xml.in b/data/com.github.tkashkin.gamehub.gschema.xml.in
index f6e2f2f2..134b2287 100644
--- a/data/com.github.tkashkin.gamehub.gschema.xml.in
+++ b/data/com.github.tkashkin.gamehub.gschema.xml.in
@@ -320,6 +320,13 @@
+
+
+ true
+ Is Steam image search enabled
+
+
+
diff --git a/po/POTFILES b/po/POTFILES
index 2caedcb6..6f75bfb7 100644
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -49,6 +49,7 @@ src/data/adapters/GamesAdapter.vala
src/data/providers/Provider.vala
src/data/providers/ImagesProvider.vala
src/data/providers/DataProvider.vala
+src/data/providers/images/Steam.vala
src/data/providers/images/SteamGridDB.vala
src/data/providers/images/JinxSGVI.vala
src/data/providers/data/IGDB.vala
diff --git a/src/app.vala b/src/app.vala
index 5739f4d2..9bb6a90d 100644
--- a/src/app.vala
+++ b/src/app.vala
@@ -143,7 +143,7 @@ namespace GameHub
GameSources = { new Steam(), new GOG(), new Humble(), new Trove(), new Itch(), new User() };
- Providers.ImageProviders = { new Providers.Images.SteamGridDB(), new Providers.Images.JinxSGVI() };
+ Providers.ImageProviders = { new Providers.Images.Steam(), new Providers.Images.SteamGridDB(), new Providers.Images.JinxSGVI() };
Providers.DataProviders = { new Providers.Data.IGDB() };
var proton_latest = new Compat.Proton(Compat.Proton.LATEST);
diff --git a/src/data/providers/images/Steam.vala b/src/data/providers/images/Steam.vala
new file mode 100644
index 00000000..8eacc12b
--- /dev/null
+++ b/src/data/providers/images/Steam.vala
@@ -0,0 +1,225 @@
+/*
+This file is part of GameHub.
+Copyright (C) 2018-2019 Anatoliy Kashkin
+
+GameHub 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 3 of the License, or
+(at your option) any later version.
+
+GameHub 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 GameHub. If not, see .
+*/
+
+using Gee;
+using GameHub.Utils;
+
+namespace GameHub.Data.Providers.Images
+{
+ public class Steam: ImagesProvider
+ {
+ private const string DOMAIN = "https://store.steampowered.com/";
+ private const string CDN_BASE_URL = "http://cdn.akamai.steamstatic.com/steam/apps/";
+ private const string API_KEY_PAGE = "https://steamcommunity.com/dev/apikey";
+ private const string API_BASE_URL = "https://api.steampowered.com/";
+
+ private const string APPLIST_CACHE_FILE = "applist.json";
+ private ImagesProvider.ImageSize?[] SIZES = { ImageSize(460, 215), ImageSize(600, 900) };
+
+ public override string id { get { return "steam"; } }
+ public override string name { get { return "Steam"; } }
+ public override string url { get { return DOMAIN; } }
+ public override string icon { get { return "source-steam-symbolic"; } }
+
+ public override bool enabled
+ {
+ get { return Settings.Providers.Images.Steam.instance.enabled; }
+ set { Settings.Providers.Images.Steam.instance.enabled = value; }
+ }
+
+ public override async ArrayList images(Game game)
+ {
+ var results = new ArrayList();
+ string? appid = null;
+
+ if(game is GameHub.Data.Sources.Steam.SteamGame)
+ {
+ appid = game.id;
+ }
+ else
+ {
+ appid = yield GameHub.Data.Sources.Steam.Steam.get_appid_from_name(game.name);
+
+ // also contains unowned games:
+ if(appid == null) appid = yield get_appid_from_name(game.name);
+ }
+
+ if(appid != null)
+ {
+ debug("[Provider.Images.Steam] Found appid %s for game %s", appid, game.name);
+ foreach(var size in SIZES)
+ {
+ var result = new ImagesProvider.Result();
+ result.images = new ArrayList();
+ result.image_size = size ?? ImageSize(460, 215);
+ result.name = "%s: %s (%d × %d)".printf(name, game.name, result.image_size.width, result.image_size.height);
+ result.url = "%sapp/%s".printf(DOMAIN, appid);
+
+ string? remote_result = null;
+ string? local_result = null;
+ switch (size.width) {
+ case 460:
+ // Always enforced by steam, exists for everything
+ local_result = yield search_local(appid);
+ remote_result = yield search_remote(appid, "header.jpg", false);
+ break;
+ // case 920:
+ // Higher resolution of the one above at the same location
+ // break;
+ case 600:
+ // Enforced since 2019, possibly not available
+ local_result = yield search_local(appid, "p");
+ remote_result = yield search_remote(appid, "library_600x900_2x.jpg");
+ break;
+ }
+
+ if(local_result != null)
+ {
+ result.images.add(new Image(local_result, "Local custom steam grid image"));
+ }
+
+ if(remote_result != null)
+ {
+ result.images.add(new Image(remote_result, "Remote download"));
+ }
+
+ if(result.images.size > 0)
+ {
+ results.add(result);
+ }
+ }
+ }
+
+ return results;
+ }
+
+ private async string? search_local(string appid, string format="")
+ {
+ string[] extensions = { ".png", ".jpg" };
+ File? griddir = Sources.Steam.Steam.get_userdata_dir().get_child("config").get_child("grid");
+
+ foreach(var extension in extensions)
+ {
+ if(griddir.get_child(appid + format + extension).query_exists())
+ {
+ return "file://" + griddir.get_child(appid + format + extension).get_path();
+ }
+ }
+
+ return null;
+ }
+
+ private async string? search_remote(string appid, string format, bool needs_check=true)
+ {
+ var exists = !needs_check;
+ var endpoint = "%s/%s".printf(appid, format);
+
+ if(needs_check)
+ {
+ exists = yield image_exists("%s%s".printf(CDN_BASE_URL, endpoint));
+ }
+
+ if(exists)
+ {
+ return "%s%s".printf(CDN_BASE_URL, endpoint);
+ }
+
+ return null;
+ }
+
+ private async bool image_exists(string url)
+ {
+ uint status;
+ yield Parser.load_remote_file_async(url, "GET", null, null, null, out status);
+ if(status == 200)
+ {
+ return true;
+ }
+ return false;
+ }
+
+ private async string? get_appid_from_name(string game_name)
+ {
+ var applist_cache_path = @"$(FSUtils.Paths.Cache.Providers)/steam/";
+ var cache_file = FSUtils.file(applist_cache_path, APPLIST_CACHE_FILE);
+ DateTime? modification_date = null;
+
+ if(cache_file.query_exists())
+ {
+ try
+ {
+ // Get modification time so we refresh only once a day
+ modification_date = cache_file.query_info("*", NONE).get_modification_date_time();
+ }
+ catch(Error e)
+ {
+ debug("[Provider.Images.Steam] %s", e.message);
+ return null;
+ }
+ }
+
+ if(!cache_file.query_exists() || modification_date == null || modification_date.compare(new DateTime.now_utc().add_days(-1)) < 0)
+ {
+ var url = @"$(API_BASE_URL)ISteamApps/GetAppList/v0002/";
+
+ FSUtils.mkdir(applist_cache_path);
+ cache_file = FSUtils.file(applist_cache_path, APPLIST_CACHE_FILE);
+
+ try
+ {
+ var json_string = yield Parser.load_remote_file_async(url);
+ var tmp = Parser.parse_json(json_string);
+ if(tmp != null && tmp.get_node_type() == Json.NodeType.OBJECT && tmp.get_object().get_object_member("applist").get_array_member("apps").get_length() > 0)
+ {
+ var dos = new DataOutputStream(cache_file.replace(null, false, FileCreateFlags.NONE));
+ dos.put_string(json_string);
+ debug("[Provider.Images.Steam] Refreshed steam applist");
+ }
+ else
+ {
+ debug("[Provider.Images.Steam] Downloaded applist is empty");
+ }
+ }
+ catch(Error e)
+ {
+ warning("[Provider.Images.Steam] %s", e.message);
+ return null;
+ }
+ }
+
+ var json = Parser.parse_json_file(applist_cache_path, APPLIST_CACHE_FILE);
+ if(json == null || json.get_node_type() != Json.NodeType.OBJECT)
+ {
+ debug("[Provider.Images.Steam] Error reading steam applist");
+ return null;
+ }
+
+ var apps = json.get_object().get_object_member("applist").get_array_member("apps").get_elements();
+ foreach(var app in apps)
+ {
+ if(app.get_object().get_string_member("name").down() == game_name.down())
+ {
+ var appid = app.get_object().get_int_member("appid").to_string();
+ return appid;
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/data/sources/steam/Steam.vala b/src/data/sources/steam/Steam.vala
index 5c0f8972..a1ecccea 100644
--- a/src/data/sources/steam/Steam.vala
+++ b/src/data/sources/steam/Steam.vala
@@ -318,6 +318,45 @@ namespace GameHub.Data.Sources.Steam
return pkgs;
}
+ public static async string? get_appid_from_name(string game_name)
+ {
+ if(instance == null) return null;
+
+ instance.load_appinfo();
+
+ if(instance.appinfo == null) return null;
+
+ foreach(var app_node in instance.appinfo.nodes.values)
+ {
+ if(app_node != null && app_node is BinaryVDF.ListNode)
+ {
+ var app = (BinaryVDF.ListNode) app_node;
+ var common_node = app.get_nested({"appinfo", "common"});
+
+ if(common_node != null && common_node is BinaryVDF.ListNode)
+ {
+ var common = (BinaryVDF.ListNode) common_node;
+
+ var name_node = common.get("name");
+ var type_node = common.get("type");
+
+ if(name_node != null && name_node is BinaryVDF.StringNode && type_node != null && type_node is BinaryVDF.StringNode)
+ {
+ var name = ((BinaryVDF.StringNode) name_node).value;
+ var type = ((BinaryVDF.StringNode) type_node).value;
+
+ if(type != null && type.down() == "game" && name != null && name.down() == game_name.down())
+ {
+ return app.key;
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
public static void install_app(string appid)
{
Utils.open_uri(@"steam://install/$(appid)");
diff --git a/src/meson.build b/src/meson.build
index 0d1f5bf8..08be0ee0 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -85,6 +85,7 @@ sources = [
'data/providers/Provider.vala',
'data/providers/ImagesProvider.vala',
'data/providers/DataProvider.vala',
+ 'data/providers/images/Steam.vala',
'data/providers/images/SteamGridDB.vala',
'data/providers/images/JinxSGVI.vala',
'data/providers/data/IGDB.vala',
diff --git a/src/settings/Providers.vala b/src/settings/Providers.vala
index a0080679..76d05f92 100644
--- a/src/settings/Providers.vala
+++ b/src/settings/Providers.vala
@@ -81,6 +81,29 @@ namespace GameHub.Settings.Providers
}
}
}
+
+ public class Steam: SettingsSchema
+ {
+ public bool enabled { get; set; }
+
+ public Steam()
+ {
+ base(ProjectConfig.PROJECT_NAME + ".providers.images.steam");
+ }
+
+ private static Steam? _instance;
+ public static unowned Steam instance
+ {
+ get
+ {
+ if(_instance == null)
+ {
+ _instance = new Steam();
+ }
+ return _instance;
+ }
+ }
+ }
}
namespace Data
diff --git a/src/utils/FSUtils.vala b/src/utils/FSUtils.vala
index 8a908f61..c90b72da 100644
--- a/src/utils/FSUtils.vala
+++ b/src/utils/FSUtils.vala
@@ -76,6 +76,8 @@ namespace GameHub.Utils
public const string WineWrap = FSUtils.Paths.Cache.Compat + "/winewrap";
public const string Sources = FSUtils.Paths.Cache.Home + "/sources";
+
+ public const string Providers = FSUtils.Paths.Cache.Home + "/providers";
}
public class LocalData