From 1f73da27ad86f2cd988694b185ab1a59d7fa7581 Mon Sep 17 00:00:00 2001 From: Lucki Date: Sat, 4 Apr 2020 15:37:26 +0200 Subject: [PATCH 1/5] Add steam image provider Former-commit-id: 6c380752c11e3892b4dbb8d13d6b4b7284bc256f --- ...com.github.tkashkin.gamehub.gschema.xml.in | 11 + po/POTFILES | 1 + src/app.vala | 2 +- src/data/providers/images/Steam.vala | 265 ++++++++++++++++++ src/meson.build | 1 + src/settings/Providers.vala | 37 +++ src/utils/FSUtils.vala | 2 + 7 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 src/data/providers/images/Steam.vala diff --git a/data/com.github.tkashkin.gamehub.gschema.xml.in b/data/com.github.tkashkin.gamehub.gschema.xml.in index f6e2f2f2..9aaecb40 100644 --- a/data/com.github.tkashkin.gamehub.gschema.xml.in +++ b/data/com.github.tkashkin.gamehub.gschema.xml.in @@ -320,6 +320,17 @@ + + + true + Is Steam image search enabled + + + '@PREF_API_KEY_STEAM@' + Steam API key + + + 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..cf6a8293 --- /dev/null +++ b/src/data/providers/images/Steam.vala @@ -0,0 +1,265 @@ +/* +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_PATH = @"$(FSUtils.Paths.Cache.Providers)/steam/"; + 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(); + var app_id = ""; + + if(game is GameHub.Data.Sources.Steam.SteamGame) + { + app_id = game.id; + } + else + { + app_id = yield get_appid(game.name); + } + + if(app_id != "") + { + foreach(var size in SIZES) + { + var needs_check = false; + var exists = false; + var result = new ImagesProvider.Result(); + 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, app_id); + + var format = "header.jpg"; + switch (size.width) { + case 460: + // Always enforced by steam, exists for everything + format = "header.jpg"; + exists = true; + break; + // case 920: + // Higher resolution of the one above at the same location + // format = "header.jpg"; + // break; + case 600: + // Enforced since 2019, possibly not available + format = "library_600x900_2x.jpg"; + needs_check = true; + break; + } + + var endpoint = "%s/%s".printf(app_id, format); + + result.images = new ArrayList(); + + if(needs_check) + { + exists = yield image_exists("%s%s".printf(CDN_BASE_URL, endpoint)); + } + + if(exists) + { + result.images.add(new Image("%s%s".printf(CDN_BASE_URL, endpoint))); + } + + if(result.images.size > 0) + { + results.add(result); + } + } + } + + return results; + } + + 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(string 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 or we get blocked really fast. + modification_date = cache_file.query_info("*", NONE).get_modification_date_time(); + } + catch(Error e) + { + debug("[Provider.Images.Steam] %s", e.message); + return ""; + } + } + + if(!cache_file.query_exists() || modification_date == null || modification_date.compare(new DateTime.now_utc().add_days(-1)) < 0) + { + // https://api.steampowered.com/ISteamApps/GetAppList/v0002/?key= + var url = @"$(API_BASE_URL)ISteamApps/GetAppList/v0002/?key=$(Settings.Providers.Images.Steam.instance.api_key)"; + + FSUtils.mkdir(applist_cache_path); + cache_file = FSUtils.file(applist_cache_path, APPLIST_CACHE_FILE); + + // https://stackoverflow.com/a/18077010 + try + { + var dos = new DataOutputStream(cache_file.replace(null, false, FileCreateFlags.NONE)); + var json_string = yield Parser.load_remote_file_async(url); + dos.put_string(json_string); + } + catch(Error e) + { + warning("[Provider.Images.Steam] %s", e.message); + return ""; + } + + debug("[Provider.Images.Steam] Refreshed steam applist"); + } + + var json = Parser.parse_json_file(applist_cache_path, APPLIST_CACHE_FILE); + if(json == null || json.get_node_type() != Json.NodeType.OBJECT) + { + return ""; + } + + try + { + // https://goessner.net/articles/JsonPath/ + // var apps = Json.Path.query("$.applist.apps[*]", json); + // var appid = ""; + // apps.get_array().foreach_element((obj, i, app) => { + // if(appid == "" && app.get_object().get_string_member("name") == name) + // { + // appid = app.get_object().get_int_member("appid").to_string(); + // debug("[Provider.Images.Steam] Found appid %s for game %s", appid, name); + // } + // }); + + // return appid; + + var apps = json.get_object().get_object_member("applist").get_array_member("apps").get_elements(); + foreach(var app in apps) + { + // exact match, maybe do some fuzzy matching? + if(app.get_object().get_string_member("name") == name) + { + var appid = app.get_object().get_int_member("appid").to_string(); + debug("[Provider.Images.Steam] Found appid %s for game %s", appid, name); + return appid; + } + } + } + catch(Error e) + { + warning("[Provider.Images.Steam] %s", e.message); + return ""; + } + + return ""; + } + + public override Gtk.Widget? settings_widget + { + owned get + { + var settings = Settings.Providers.Images.Steam.instance; + + var grid = new Gtk.Grid(); + grid.column_spacing = 12; + grid.row_spacing = 4; + + var entry = new Gtk.Entry(); + entry.placeholder_text = _("Default"); + entry.max_length = 32; + if(settings.api_key != settings.schema.get_default_value("api-key").get_string()) + { + entry.text = settings.api_key; + } + entry.secondary_icon_name = "edit-delete-symbolic"; + entry.secondary_icon_tooltip_text = _("Restore default API key"); + entry.set_size_request(250, -1); + + entry.notify["text"].connect(() => { settings.api_key = entry.text; }); + entry.icon_press.connect((pos, e) => { + if(pos == Gtk.EntryIconPosition.SECONDARY) + { + entry.text = ""; + } + }); + + var label = new Gtk.Label(_("API key")); + label.halign = Gtk.Align.START; + label.valign = Gtk.Align.CENTER; + label.hexpand = true; + + var entry_wrapper = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0); + entry_wrapper.get_style_context().add_class(Gtk.STYLE_CLASS_LINKED); + + var link = new Gtk.Button.with_label(_("Generate key")); + link.tooltip_text = API_KEY_PAGE; + + link.clicked.connect(() => { + Utils.open_uri(API_KEY_PAGE); + }); + + entry_wrapper.add(entry); + entry_wrapper.add(link); + + grid.attach(label, 0, 0); + grid.attach(entry_wrapper, 1, 0); + + return grid; + } + } + } +} 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..082f042a 100644 --- a/src/settings/Providers.vala +++ b/src/settings/Providers.vala @@ -81,6 +81,43 @@ namespace GameHub.Settings.Providers } } } + + public class Steam: SettingsSchema + { + public bool enabled { get; set; } + public string api_key { get; set; } + + public Steam() + { + base(ProjectConfig.PROJECT_NAME + ".providers.images.steam"); + } + + protected override void verify(string key) + { + switch(key) + { + case "api-key": + if(api_key.length != 32) + { + schema.reset("api-key"); + } + break; + } + } + + 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 From 291bd667d5d156aa4cbbec32c11d022538ab099a Mon Sep 17 00:00:00 2001 From: Lucki Date: Sun, 5 Apr 2020 03:36:05 +0200 Subject: [PATCH 2/5] Catch possible empty applist Former-commit-id: 55f6f723a4736bbba7890e0544aab06abf38dc59 --- src/data/providers/images/Steam.vala | 56 +++++++++++----------------- 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/src/data/providers/images/Steam.vala b/src/data/providers/images/Steam.vala index cf6a8293..dd31b5a5 100644 --- a/src/data/providers/images/Steam.vala +++ b/src/data/providers/images/Steam.vala @@ -24,7 +24,7 @@ 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 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/"; @@ -132,7 +132,7 @@ namespace GameHub.Data.Providers.Images { try { - // Get modification time so we refresh only once a day or we get blocked really fast. + // Get modification time so we refresh only once a day modification_date = cache_file.query_info("*", NONE).get_modification_date_time(); } catch(Error e) @@ -150,60 +150,46 @@ namespace GameHub.Data.Providers.Images FSUtils.mkdir(applist_cache_path); cache_file = FSUtils.file(applist_cache_path, APPLIST_CACHE_FILE); - // https://stackoverflow.com/a/18077010 try { - var dos = new DataOutputStream(cache_file.replace(null, false, FileCreateFlags.NONE)); var json_string = yield Parser.load_remote_file_async(url); - dos.put_string(json_string); + 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 ""; } - - debug("[Provider.Images.Steam] Refreshed steam applist"); } 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 ""; } - try + var apps = json.get_object().get_object_member("applist").get_array_member("apps").get_elements(); + foreach(var app in apps) { - // https://goessner.net/articles/JsonPath/ - // var apps = Json.Path.query("$.applist.apps[*]", json); - // var appid = ""; - // apps.get_array().foreach_element((obj, i, app) => { - // if(appid == "" && app.get_object().get_string_member("name") == name) - // { - // appid = app.get_object().get_int_member("appid").to_string(); - // debug("[Provider.Images.Steam] Found appid %s for game %s", appid, name); - // } - // }); - - // return appid; - - var apps = json.get_object().get_object_member("applist").get_array_member("apps").get_elements(); - foreach(var app in apps) + // exact match, maybe do some fuzzy matching? + if(app.get_object().get_string_member("name") == name) { - // exact match, maybe do some fuzzy matching? - if(app.get_object().get_string_member("name") == name) - { - var appid = app.get_object().get_int_member("appid").to_string(); - debug("[Provider.Images.Steam] Found appid %s for game %s", appid, name); - return appid; - } + var appid = app.get_object().get_int_member("appid").to_string(); + debug("[Provider.Images.Steam] Found appid %s for game %s", appid, name); + return appid; } } - catch(Error e) - { - warning("[Provider.Images.Steam] %s", e.message); - return ""; - } return ""; } From e985739aa2620173e7ad0b958de39548394e0237 Mon Sep 17 00:00:00 2001 From: Lucki Date: Sun, 5 Apr 2020 15:55:38 +0200 Subject: [PATCH 3/5] get appids from appinfo.vdf Former-commit-id: 046240e254fc02b3b7ca050180e695152f459eff --- ...com.github.tkashkin.gamehub.gschema.xml.in | 4 - src/data/providers/images/Steam.vala | 144 +----------------- src/data/sources/steam/Steam.vala | 39 +++++ src/settings/Providers.vala | 14 -- src/utils/FSUtils.vala | 2 - 5 files changed, 46 insertions(+), 157 deletions(-) diff --git a/data/com.github.tkashkin.gamehub.gschema.xml.in b/data/com.github.tkashkin.gamehub.gschema.xml.in index 9aaecb40..134b2287 100644 --- a/data/com.github.tkashkin.gamehub.gschema.xml.in +++ b/data/com.github.tkashkin.gamehub.gschema.xml.in @@ -325,10 +325,6 @@ true Is Steam image search enabled - - '@PREF_API_KEY_STEAM@' - Steam API key - diff --git a/src/data/providers/images/Steam.vala b/src/data/providers/images/Steam.vala index dd31b5a5..ecea8077 100644 --- a/src/data/providers/images/Steam.vala +++ b/src/data/providers/images/Steam.vala @@ -25,11 +25,6 @@ namespace GameHub.Data.Providers.Images { 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_PATH = @"$(FSUtils.Paths.Cache.Providers)/steam/"; - private const string APPLIST_CACHE_FILE = "applist.json"; private ImagesProvider.ImageSize?[] SIZES = { ImageSize(460, 215), ImageSize(600, 900) }; @@ -47,19 +42,20 @@ namespace GameHub.Data.Providers.Images public override async ArrayList images(Game game) { var results = new ArrayList(); - var app_id = ""; + string? appid = null; if(game is GameHub.Data.Sources.Steam.SteamGame) { - app_id = game.id; + appid = game.id; } else { - app_id = yield get_appid(game.name); + appid = yield GameHub.Data.Sources.Steam.Steam.get_appid_from_name(game.name); } - if(app_id != "") + if(appid != null) { + debug("[Provider.Images.Steam] Found appid %s for game %s", appid, game.name); foreach(var size in SIZES) { var needs_check = false; @@ -67,7 +63,7 @@ namespace GameHub.Data.Providers.Images var result = new ImagesProvider.Result(); 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, app_id); + result.url = "%sapp/%s".printf(DOMAIN, appid); var format = "header.jpg"; switch (size.width) { @@ -87,7 +83,7 @@ namespace GameHub.Data.Providers.Images break; } - var endpoint = "%s/%s".printf(app_id, format); + var endpoint = "%s/%s".printf(appid, format); result.images = new ArrayList(); @@ -121,131 +117,5 @@ namespace GameHub.Data.Providers.Images } return false; } - - private async string get_appid(string 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 ""; - } - } - - if(!cache_file.query_exists() || modification_date == null || modification_date.compare(new DateTime.now_utc().add_days(-1)) < 0) - { - // https://api.steampowered.com/ISteamApps/GetAppList/v0002/?key= - var url = @"$(API_BASE_URL)ISteamApps/GetAppList/v0002/?key=$(Settings.Providers.Images.Steam.instance.api_key)"; - - 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 ""; - } - } - - 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 ""; - } - - var apps = json.get_object().get_object_member("applist").get_array_member("apps").get_elements(); - foreach(var app in apps) - { - // exact match, maybe do some fuzzy matching? - if(app.get_object().get_string_member("name") == name) - { - var appid = app.get_object().get_int_member("appid").to_string(); - debug("[Provider.Images.Steam] Found appid %s for game %s", appid, name); - return appid; - } - } - - return ""; - } - - public override Gtk.Widget? settings_widget - { - owned get - { - var settings = Settings.Providers.Images.Steam.instance; - - var grid = new Gtk.Grid(); - grid.column_spacing = 12; - grid.row_spacing = 4; - - var entry = new Gtk.Entry(); - entry.placeholder_text = _("Default"); - entry.max_length = 32; - if(settings.api_key != settings.schema.get_default_value("api-key").get_string()) - { - entry.text = settings.api_key; - } - entry.secondary_icon_name = "edit-delete-symbolic"; - entry.secondary_icon_tooltip_text = _("Restore default API key"); - entry.set_size_request(250, -1); - - entry.notify["text"].connect(() => { settings.api_key = entry.text; }); - entry.icon_press.connect((pos, e) => { - if(pos == Gtk.EntryIconPosition.SECONDARY) - { - entry.text = ""; - } - }); - - var label = new Gtk.Label(_("API key")); - label.halign = Gtk.Align.START; - label.valign = Gtk.Align.CENTER; - label.hexpand = true; - - var entry_wrapper = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0); - entry_wrapper.get_style_context().add_class(Gtk.STYLE_CLASS_LINKED); - - var link = new Gtk.Button.with_label(_("Generate key")); - link.tooltip_text = API_KEY_PAGE; - - link.clicked.connect(() => { - Utils.open_uri(API_KEY_PAGE); - }); - - entry_wrapper.add(entry); - entry_wrapper.add(link); - - grid.attach(label, 0, 0); - grid.attach(entry_wrapper, 1, 0); - - return grid; - } - } } } 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/settings/Providers.vala b/src/settings/Providers.vala index 082f042a..76d05f92 100644 --- a/src/settings/Providers.vala +++ b/src/settings/Providers.vala @@ -85,26 +85,12 @@ namespace GameHub.Settings.Providers public class Steam: SettingsSchema { public bool enabled { get; set; } - public string api_key { get; set; } public Steam() { base(ProjectConfig.PROJECT_NAME + ".providers.images.steam"); } - protected override void verify(string key) - { - switch(key) - { - case "api-key": - if(api_key.length != 32) - { - schema.reset("api-key"); - } - break; - } - } - private static Steam? _instance; public static unowned Steam instance { diff --git a/src/utils/FSUtils.vala b/src/utils/FSUtils.vala index c90b72da..8a908f61 100644 --- a/src/utils/FSUtils.vala +++ b/src/utils/FSUtils.vala @@ -76,8 +76,6 @@ 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 From 47722625f69ff64cfdb10751dcae5000262560d7 Mon Sep 17 00:00:00 2001 From: Lucki Date: Sun, 5 Apr 2020 16:39:27 +0200 Subject: [PATCH 4/5] fallback for unowned steam games Former-commit-id: c980806d09f72cc1425805d2f475c0ae1022576d --- src/data/providers/images/Steam.vala | 75 ++++++++++++++++++++++++++++ src/utils/FSUtils.vala | 2 + 2 files changed, 77 insertions(+) diff --git a/src/data/providers/images/Steam.vala b/src/data/providers/images/Steam.vala index ecea8077..00edb706 100644 --- a/src/data/providers/images/Steam.vala +++ b/src/data/providers/images/Steam.vala @@ -25,7 +25,10 @@ namespace GameHub.Data.Providers.Images { 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"; } } @@ -51,6 +54,9 @@ namespace GameHub.Data.Providers.Images 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) @@ -117,5 +123,74 @@ namespace GameHub.Data.Providers.Images } 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/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 From 91a39cb8ba8be8a867310e338b497df78aa55713 Mon Sep 17 00:00:00 2001 From: Lucki Date: Tue, 14 Apr 2020 16:42:07 +0200 Subject: [PATCH 5/5] Add local custom grid images as results Former-commit-id: c9dc5476f81460a1ef4043133197368edaea431a --- src/data/providers/images/Steam.vala | 61 ++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/src/data/providers/images/Steam.vala b/src/data/providers/images/Steam.vala index 00edb706..8eacc12b 100644 --- a/src/data/providers/images/Steam.vala +++ b/src/data/providers/images/Steam.vala @@ -64,43 +64,38 @@ namespace GameHub.Data.Providers.Images debug("[Provider.Images.Steam] Found appid %s for game %s", appid, game.name); foreach(var size in SIZES) { - var needs_check = false; - var exists = false; 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); - var format = "header.jpg"; + string? remote_result = null; + string? local_result = null; switch (size.width) { case 460: // Always enforced by steam, exists for everything - format = "header.jpg"; - exists = true; + 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 - // format = "header.jpg"; // break; case 600: // Enforced since 2019, possibly not available - format = "library_600x900_2x.jpg"; - needs_check = true; + local_result = yield search_local(appid, "p"); + remote_result = yield search_remote(appid, "library_600x900_2x.jpg"); break; } - var endpoint = "%s/%s".printf(appid, format); - - result.images = new ArrayList(); - - if(needs_check) + if(local_result != null) { - exists = yield image_exists("%s%s".printf(CDN_BASE_URL, endpoint)); + result.images.add(new Image(local_result, "Local custom steam grid image")); } - if(exists) + if(remote_result != null) { - result.images.add(new Image("%s%s".printf(CDN_BASE_URL, endpoint))); + result.images.add(new Image(remote_result, "Remote download")); } if(result.images.size > 0) @@ -113,6 +108,40 @@ namespace GameHub.Data.Providers.Images 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;