Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add steam image provider #363

Merged
merged 5 commits into from
Apr 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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)");
Expand Down
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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',
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