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