Skip to content

Commit

Permalink
Merge pull request #363 from Lucki/steam_images
Browse files Browse the repository at this point in the history
Add steam image provider

Former-commit-id: 19bbd48
  • Loading branch information
tkashkin committed Apr 15, 2020
2 parents 50dd079 + 91a39cb commit 1b0a60b
Show file tree
Hide file tree
Showing 8 changed files with 299 additions and 1 deletion.
7 changes: 7 additions & 0 deletions data/com.github.tkashkin.gamehub.gschema.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,13 @@
</key>
</schema>

<schema path="@SCHEMA_PATH@/providers/images/steam/" id="@SCHEMA_ID@.providers.images.steam">
<key name="enabled" type="b">
<default>true</default>
<summary>Is Steam image search enabled</summary>
</key>
</schema>

<!-- Providers / Data / IGDB -->
<enum id="@SCHEMA_ID@.providers.data.igdb.preferred-description">
<value nick="Game" value="0" />
Expand Down
1 change: 1 addition & 0 deletions po/POTFILES
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/app.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
225 changes: 225 additions & 0 deletions src/data/providers/images/Steam.vala
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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<ImagesProvider.Result> images(Game game)
{
var results = new ArrayList<ImagesProvider.Result>();
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<ImagesProvider.Image>();
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;
}
}
}
39 changes: 39 additions & 0 deletions src/data/sources/steam/Steam.vala
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,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)");
Expand Down
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,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',
Expand Down
23 changes: 23 additions & 0 deletions src/settings/Providers.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/utils/FSUtils.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 1b0a60b

Please sign in to comment.