From df0d8a81c0edec460cb1060b787353b134a8e620 Mon Sep 17 00:00:00 2001 From: tkashkin Date: Sun, 17 Jun 2018 07:11:24 +0300 Subject: [PATCH] Humble Bundle support 0.3.0 release Former-commit-id: 89968b4fdc32bf4fe84a048bbddf64a4dcc5f218 --- README.md | 1 + ...com.github.tkashkin.gamehub.appdata.xml.in | 5 + data/com.github.tkashkin.gamehub.gschema.xml | 12 + data/icons/humble-white.svg | 1 + data/icons/humble.svg | 1 + data/icons/icons.gresource.xml | 2 + debian/changelog | 6 + meson.build | 2 +- po/POTFILES | 9 +- po/com.github.tkashkin.gamehub.pot | 29 ++- po/ru.po | 39 +-- src/app.vala | 3 +- src/data/Game.vala | 11 +- src/data/GamesDB.vala | 17 +- src/data/sources/gog/GOG.vala | 11 +- src/data/sources/gog/GOGGame.vala | 25 +- src/data/sources/humble/Humble.vala | 133 +++++++++++ src/data/sources/humble/HumbleGame.vala | 225 ++++++++++++++++++ src/data/sources/steam/Steam.vala | 5 +- src/data/sources/steam/SteamGame.vala | 5 +- src/meson.build | 5 +- ...tallDialog.vala => GameInstallDialog.vala} | 33 +-- src/ui/dialogs/SettingsDialog.vala | 4 + src/ui/windows/WebAuthWindow.vala | 27 ++- src/utils/FSUtils.vala | 13 +- src/utils/Parser.vala | 63 ++--- src/utils/Settings.vala | 21 ++ 27 files changed, 603 insertions(+), 105 deletions(-) create mode 100644 data/icons/humble-white.svg create mode 100644 data/icons/humble.svg create mode 100644 src/data/sources/humble/Humble.vala create mode 100644 src/data/sources/humble/HumbleGame.vala rename src/ui/dialogs/{GOGGameInstallDialog.vala => GameInstallDialog.vala} (63%) diff --git a/README.md b/README.md index 4d1014b7..1c7d6fdb 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ GameHub can support multiple game sources and services __Currently supported sources:__ * Steam * GOG.com +* Humble Bundle ## Features __Implemented:__ diff --git a/data/com.github.tkashkin.gamehub.appdata.xml.in b/data/com.github.tkashkin.gamehub.appdata.xml.in index 3727cb5b..bfa37dbc 100644 --- a/data/com.github.tkashkin.gamehub.appdata.xml.in +++ b/data/com.github.tkashkin.gamehub.appdata.xml.in @@ -40,6 +40,11 @@ + + +

Humble Bundle support

+
+

Bug fixes

diff --git a/data/com.github.tkashkin.gamehub.gschema.xml b/data/com.github.tkashkin.gamehub.gschema.xml index 96313372..950d30cb 100644 --- a/data/com.github.tkashkin.gamehub.gschema.xml +++ b/data/com.github.tkashkin.gamehub.gschema.xml @@ -51,6 +51,15 @@ + + + false + + + '' + + + '~/.steam' @@ -58,5 +67,8 @@ '~/Games/GOG' + + '~/Games/HumbleBundle' + diff --git a/data/icons/humble-white.svg b/data/icons/humble-white.svg new file mode 100644 index 00000000..85dc0f5f --- /dev/null +++ b/data/icons/humble-white.svg @@ -0,0 +1 @@ + diff --git a/data/icons/humble.svg b/data/icons/humble.svg new file mode 100644 index 00000000..78a2ba24 --- /dev/null +++ b/data/icons/humble.svg @@ -0,0 +1 @@ + diff --git a/data/icons/icons.gresource.xml b/data/icons/icons.gresource.xml index 566fd5b4..a7e5e5f0 100644 --- a/data/icons/icons.gresource.xml +++ b/data/icons/icons.gresource.xml @@ -3,8 +3,10 @@ steam.svg gog.svg + humble.svg steam-white.svg gog-white.svg + humble-white.svg diff --git a/debian/changelog b/debian/changelog index ed1e7da5..78e98166 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +com.github.tkashkin.gamehub (0.3.0) xenial; urgency=low + + * Humble Bundle support + + -- tkashkin Sun, 17 Jun 2018 04:57:12 +0300 + com.github.tkashkin.gamehub (0.2.5) xenial; urgency=low * Bug fixes diff --git a/meson.build b/meson.build index 7bfd73ef..b3d2418f 100644 --- a/meson.build +++ b/meson.build @@ -1,4 +1,4 @@ -project('com.github.tkashkin.gamehub', 'vala', 'c', version: '0.2.5') +project('com.github.tkashkin.gamehub', 'vala', 'c', version: '0.3.0') i18n = import('i18n') gnome = import('gnome') diff --git a/po/POTFILES b/po/POTFILES index d7d1d4ea..395e3d70 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -10,16 +10,19 @@ src/data/GameSource.vala src/data/sources/steam/Steam.vala src/data/sources/steam/SteamGame.vala -src/data/GamesDB.vala - src/data/sources/gog/GOG.vala src/data/sources/gog/GOGGame.vala +src/data/sources/humble/Humble.vala +src/data/sources/humble/HumbleGame.vala + +src/data/GamesDB.vala + src/ui/windows/MainWindow.vala src/ui/windows/WebAuthWindow.vala src/ui/dialogs/SettingsDialog.vala -src/ui/dialogs/GOGGameInstallDialog.vala +src/ui/dialogs/GameInstallDialog.vala src/ui/views/BaseView.vala src/ui/views/WelcomeView.vala diff --git a/po/com.github.tkashkin.gamehub.pot b/po/com.github.tkashkin.gamehub.pot index 3023f1c6..6f3d771c 100644 --- a/po/com.github.tkashkin.gamehub.pot +++ b/po/com.github.tkashkin.gamehub.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.github.tkashkin.gamehub\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-06-02 05:53+0300\n" +"POT-Creation-Date: 2018-06-17 04:53+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -50,6 +50,19 @@ msgstr "" msgid "Your SteamID will be read from Steam configuration file" msgstr "" +#: src/data/sources/humble/HumbleGame.vala:163 +msgid "Select main executable of the game" +msgstr "" + +#: src/data/sources/humble/HumbleGame.vala:164 +#: src/ui/dialogs/GameInstallDialog.vala:75 +msgid "Cancel" +msgstr "" + +#: src/data/sources/humble/HumbleGame.vala:164 +msgid "Select" +msgstr "" + #: src/ui/dialogs/SettingsDialog.vala:27 msgid "Use dark theme" msgstr "" @@ -74,19 +87,19 @@ msgstr "" msgid "GOG games directory" msgstr "" -#: src/ui/dialogs/SettingsDialog.vala:50 -msgid "Close" +#: src/ui/dialogs/SettingsDialog.vala:41 +msgid "Humble Bundle games directory" msgstr "" -#: src/ui/dialogs/GOGGameInstallDialog.vala:47 -msgid "Select game language" +#: src/ui/dialogs/SettingsDialog.vala:54 +msgid "Close" msgstr "" -#: src/ui/dialogs/GOGGameInstallDialog.vala:69 -msgid "Cancel" +#: src/ui/dialogs/GameInstallDialog.vala:48 +msgid "Select game installer" msgstr "" -#: src/ui/dialogs/GOGGameInstallDialog.vala:70 +#: src/ui/dialogs/GameInstallDialog.vala:76 msgid "Install" msgstr "" diff --git a/po/ru.po b/po/ru.po index c1b6609d..10302637 100644 --- a/po/ru.po +++ b/po/ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.github.tkashkin.gamehub\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-06-02 05:53+0300\n" +"POT-Creation-Date: 2018-06-17 04:53+0300\n" "PO-Revision-Date: 2018-05-27 03:39+0300\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -18,12 +18,6 @@ msgstr "" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" -#: data/com.github.tkashkin.gamehub.appdata.xml.in:8 -#: data/com.github.tkashkin.gamehub.desktop.in:3 -#: data/com.github.tkashkin.gamehub.desktop.in:4 -msgid "GameHub" -msgstr "" - #: data/com.github.tkashkin.gamehub.appdata.xml.in:9 #: data/com.github.tkashkin.gamehub.desktop.in:5 #: src/ui/views/WelcomeView.vala:32 @@ -38,6 +32,19 @@ msgstr "Управляйте играми из Steam и GOG в одном мес msgid "Your SteamID will be read from Steam configuration file" msgstr "Ваш SteamID будет прочитан из файла конфигурации Steam" +#: src/data/sources/humble/HumbleGame.vala:163 +msgid "Select main executable of the game" +msgstr "Выберите исполняемый файл игры" + +#: src/data/sources/humble/HumbleGame.vala:164 +#: src/ui/dialogs/GameInstallDialog.vala:75 +msgid "Cancel" +msgstr "Отмена" + +#: src/data/sources/humble/HumbleGame.vala:164 +msgid "Select" +msgstr "Выбрать" + #: src/ui/dialogs/SettingsDialog.vala:27 msgid "Use dark theme" msgstr "Использовать тёмную тему" @@ -62,19 +69,19 @@ msgstr "Папка установки Steam" msgid "GOG games directory" msgstr "Папка игр GOG" -#: src/ui/dialogs/SettingsDialog.vala:50 +#: src/ui/dialogs/SettingsDialog.vala:41 +msgid "Humble Bundle games directory" +msgstr "Папка игр Humble Bundle" + +#: src/ui/dialogs/SettingsDialog.vala:54 msgid "Close" msgstr "Закрыть" -#: src/ui/dialogs/GOGGameInstallDialog.vala:47 -msgid "Select game language" -msgstr "Выберите язык игры" - -#: src/ui/dialogs/GOGGameInstallDialog.vala:69 -msgid "Cancel" -msgstr "Отмена" +#: src/ui/dialogs/GameInstallDialog.vala:48 +msgid "Select game installer" +msgstr "Выберите установочный файл игры" -#: src/ui/dialogs/GOGGameInstallDialog.vala:70 +#: src/ui/dialogs/GameInstallDialog.vala:76 msgid "Install" msgstr "Установить" diff --git a/src/app.vala b/src/app.vala index bfa3abd6..d1c24d3a 100644 --- a/src/app.vala +++ b/src/app.vala @@ -5,6 +5,7 @@ using Granite; using GameHub.Data; using GameHub.Data.Sources.Steam; using GameHub.Data.Sources.GOG; +using GameHub.Data.Sources.Humble; using GameHub.Utils; namespace GameHub @@ -42,7 +43,7 @@ namespace GameHub GamesDB.init(); - GameSources = { new Steam(), new GOG() }; + GameSources = { new Steam(), new GOG(), new Humble() }; var app = new Application(); return app.run(args); diff --git a/src/data/Game.vala b/src/data/Game.vala index c1420361..595620c8 100644 --- a/src/data/Game.vala +++ b/src/data/Game.vala @@ -12,7 +12,7 @@ namespace GameHub.Data public string icon { get; protected set; } public string image { get; protected set; } - public string playtime { get; protected set; default = ""; } + public string custom_info { get; protected set; default = ""; } public virtual async bool is_for_linux(){ return true; } @@ -25,5 +25,14 @@ namespace GameHub.Data { return first == second || (first.source == second.source && first.id == second.id); } + + public abstract class Installer + { + public string id { get; protected set; } + public string os { get; protected set; } + public string file { get; protected set; } + + public virtual string name { get { return id; } } + } } } diff --git a/src/data/GamesDB.vala b/src/data/GamesDB.vala index 549a54af..f9e303b9 100644 --- a/src/data/GamesDB.vala +++ b/src/data/GamesDB.vala @@ -7,6 +7,7 @@ using GameHub.Utils; using GameHub.Data.Sources.Steam; using GameHub.Data.Sources.GOG; +using GameHub.Data.Sources.Humble; namespace GameHub.Data { @@ -28,14 +29,20 @@ namespace GameHub.Data public void create_tables() requires (db != null) { - db.exec("CREATE TABLE IF NOT EXISTS `games`(`source` string not null, `id` string not null, `name` string not null, `icon` string, `image` string, `playtime` string, PRIMARY KEY(`source`, `id`))"); + Statement stmt; + if(db.prepare_v2("SELECT `playtime` FROM `games`", -1, out stmt) == Sqlite.OK) // migrate from v1 + { + db.exec("DROP TABLE `games`"); + } + + db.exec("CREATE TABLE IF NOT EXISTS `games`(`source` string not null, `id` string not null, `name` string not null, `icon` string, `image` string, `custom_info` string, PRIMARY KEY(`source`, `id`))"); db.exec("CREATE TABLE IF NOT EXISTS `unsupported_games`(`source` string not null, `id` string not null, PRIMARY KEY(`source`, `id`))"); } public bool add_game(Game game) requires (db != null) { Statement stmt; - int res = db.prepare_v2("INSERT OR REPLACE INTO `games`(`source`, `id`, `name`, `icon`, `image`, `playtime`) VALUES (?, ?, ?, ?, ?, ?)", -1, out stmt); + int res = db.prepare_v2("INSERT OR REPLACE INTO `games`(`source`, `id`, `name`, `icon`, `image`, `custom_info`) VALUES (?, ?, ?, ?, ?, ?)", -1, out stmt); assert(res == Sqlite.OK); stmt.bind_text(1, game.source.name); @@ -43,7 +50,7 @@ namespace GameHub.Data stmt.bind_text(3, game.name); stmt.bind_text(4, game.icon); stmt.bind_text(5, game.image); - stmt.bind_text(6, game.playtime); + stmt.bind_text(6, game.custom_info); res = stmt.step(); @@ -95,6 +102,10 @@ namespace GameHub.Data { games.add(new GOGGame.from_db((GOG) s, stmt)); } + else if(s is Humble) + { + games.add(new HumbleGame.from_db((Humble) s, stmt)); + } } return games; diff --git a/src/data/sources/gog/GOG.vala b/src/data/sources/gog/GOG.vala index 531e7153..8b3be1e6 100644 --- a/src/data/sources/gog/GOG.vala +++ b/src/data/sources/gog/GOG.vala @@ -48,7 +48,7 @@ namespace GameHub.Data.Sources.GOG public override async bool authenticate() { - Settings.Auth.GOG.get_instance().authenticated = true; + settings.authenticated = true; if(token_needs_refresh && user_refresh_token != null) { @@ -65,7 +65,7 @@ namespace GameHub.Data.Sources.GOG public override bool can_authenticate_automatically() { - return user_refresh_token != null && Settings.Auth.GOG.get_instance().authenticated; + return user_refresh_token != null && settings.authenticated; } private async bool get_auth_code() @@ -102,7 +102,7 @@ namespace GameHub.Data.Sources.GOG } var url = @"https://auth.gog.com/token?client_id=$(CLIENT_ID)&client_secret=$(CLIENT_SECRET)&grant_type=authorization_code&redirect_uri=$(REDIRECT)&code=$(user_auth_code)"; - var root = yield Parser.parse_remote_json_file_async(url); + var root = (yield Parser.parse_remote_json_file_async(url)).get_object(); user_token = root.get_string_member("access_token"); user_refresh_token = root.get_string_member("refresh_token"); user_id = root.get_string_member("user_id"); @@ -121,7 +121,7 @@ namespace GameHub.Data.Sources.GOG } var url = @"https://auth.gog.com/token?client_id=$(CLIENT_ID)&client_secret=$(CLIENT_SECRET)&grant_type=refresh_token&refresh_token=$(user_refresh_token)"; - var root = yield Parser.parse_remote_json_file_async(url); + var root = (yield Parser.parse_remote_json_file_async(url)).get_object(); user_token = root.get_string_member("access_token"); user_refresh_token = root.get_string_member("refresh_token"); user_id = root.get_string_member("user_id"); @@ -156,9 +156,10 @@ namespace GameHub.Data.Sources.GOG } } } + games_count = games.size; var url = @"https://embed.gog.com/account/getFilteredProducts?mediaType=1"; - var root = yield Parser.parse_remote_json_file_async(url, "GET", user_token); + var root = (yield Parser.parse_remote_json_file_async(url, "GET", user_token)).get_object(); var products = root.get_array_member("products"); diff --git a/src/data/sources/gog/GOGGame.vala b/src/data/sources/gog/GOGGame.vala index 9b543589..a524d08b 100644 --- a/src/data/sources/gog/GOGGame.vala +++ b/src/data/sources/gog/GOGGame.vala @@ -27,7 +27,11 @@ namespace GameHub.Data.Sources.GOG icon = image; _is_for_linux = json.get_object_member("worksOn").get_boolean_member("Linux"); - if(!_is_for_linux) GamesDB.get_instance().add_unsupported_game(source, id); + if(!_is_for_linux) + { + GamesDB.get_instance().add_unsupported_game(source, id); + return; + } executable = FSUtils.file(FSUtils.Paths.GOG.Games, installation_dir_name + "/start.sh"); } @@ -39,7 +43,7 @@ namespace GameHub.Data.Sources.GOG name = stmt.column_text(2); icon = stmt.column_text(3); image = stmt.column_text(4); - playtime = stmt.column_text(5); + custom_info = stmt.column_text(5); _is_for_linux = true; executable = FSUtils.file(FSUtils.Paths.GOG.Games, installation_dir_name + "/start.sh"); } @@ -57,13 +61,13 @@ namespace GameHub.Data.Sources.GOG public override async void install(DownloadProgress progress = (d, t) => {}) { var url = @"https://api.gog.com/products/$(id)?expand=downloads"; - var root = yield Parser.parse_remote_json_file_async(url, "GET", ((GOG) source).user_token); + var root = (yield Parser.parse_remote_json_file_async(url, "GET", ((GOG) source).user_token)).get_object(); icon = "https:" + root.get_object_member("images").get_string_member("icon"); var installers_json = root.get_object_member("downloads").get_array_member("installers"); - var installers = new ArrayList(); + var installers = new ArrayList(); foreach(var installer_json in installers_json.get_elements()) { @@ -71,12 +75,12 @@ namespace GameHub.Data.Sources.GOG if(installer.os == "linux") installers.add(installer); } - var wnd = new GameHub.UI.Dialogs.GOGGameInstallDialog(this, installers); + var wnd = new GameHub.UI.Dialogs.GameInstallDialog(this, installers); wnd.canceled.connect(() => Idle.add(install.callback)); wnd.install.connect(installer => { - root = Parser.parse_remote_json_file(installer.file, "GET", ((GOG) source).user_token); + root = Parser.parse_remote_json_file(installer.file, "GET", ((GOG) source).user_token).get_object(); var link = root.get_string_member("downlink"); var local = FSUtils.expand(FSUtils.Paths.GOG.Installers, "gog_" + id + "_" + installer.id + ".sh"); @@ -89,7 +93,7 @@ namespace GameHub.Data.Sources.GOG var file = Downloader.get_instance().download.end(res).get_path(); var install_dir = FSUtils.expand(FSUtils.Paths.GOG.Games, installation_dir_name); Utils.run(@"chmod +x \"$(file)\""); - Utils.run_async.begin(@"$(file) -- --i-agree-to-all-licenses --noreadme --nooptions --noprompt --destination \"$(install_dir)\"", (obj, res) => { + Utils.run_async.begin(@"$(file) -- --i-agree-to-all-licenses --noreadme --nooptions --noprompt --destination $(install_dir)", (obj, res) => { Utils.run_async.end(res); Idle.add(install.callback); }); @@ -116,13 +120,12 @@ namespace GameHub.Data.Sources.GOG } } - public class Installer + public class Installer: Game.Installer { - public string id; - public string os; public string lang; public string lang_full; - public string file; + + public override string name { get { return lang_full; } } public Installer(Json.Object json) { diff --git a/src/data/sources/humble/Humble.vala b/src/data/sources/humble/Humble.vala new file mode 100644 index 00000000..c69c4e13 --- /dev/null +++ b/src/data/sources/humble/Humble.vala @@ -0,0 +1,133 @@ +using Gtk; +using Gee; +using GameHub.Utils; + +namespace GameHub.Data.Sources.Humble +{ + public class Humble: GameSource + { + public const string AUTH_COOKIE = "_simpleauth_sess"; + + public override string name { get { return "Humble Bundle"; } } + public override string icon { get { return "humble"; } } + + public string? user_token = null; + + private Settings.Auth.Humble settings; + + public Humble() + { + settings = Settings.Auth.Humble.get_instance(); + var access_token = settings.access_token; + if(access_token.length > 0) + { + user_token = access_token; + } + } + + public override bool is_installed(bool refresh) + { + return true; + } + + public override async bool install() + { + return true; + } + + public override async bool authenticate() + { + settings.authenticated = true; + + return yield get_token(); + } + + public override bool is_authenticated() + { + return user_token != null; + } + + public override bool can_authenticate_automatically() + { + return user_token != null && settings.authenticated; + } + + private async bool get_token() + { + if(user_token != null) + { + return true; + } + + var wnd = new GameHub.UI.Windows.WebAuthWindow(this.name, @"https://www.humblebundle.com/login?goto=home", null, AUTH_COOKIE); + + wnd.finished.connect(token => + { + user_token = token; + settings.access_token = user_token ?? ""; + Idle.add(get_token.callback); + }); + + wnd.canceled.connect(() => Idle.add(get_token.callback)); + + wnd.show_all(); + wnd.present(); + + yield; + + return user_token != null; + } + + private ArrayList games = new ArrayList(Game.is_equal); + public override async ArrayList load_games(FutureResult? game_loaded = null) + { + if(user_token == null || games.size > 0) + { + return games; + } + + games.clear(); + + var cached = GamesDB.get_instance().get_games(this); + if(cached.size > 0) + { + games = cached; + if(game_loaded != null) + { + foreach(var g in cached) + { + game_loaded(g); + } + } + } + games_count = games.size; + + var headers = new HashMap(); + headers["Cookie"] = @"$(AUTH_COOKIE)=\"$(user_token)\";"; + + var orders = (yield Parser.parse_remote_json_file_async("https://www.humblebundle.com/api/v1/user/order?ajax=true", "GET", null, headers)).get_array(); + + foreach(var order in orders.get_elements()) + { + var key = order.get_object().get_string_member("gamekey"); + + var root = (yield Parser.parse_remote_json_file_async(@"https://www.humblebundle.com/api/v1/order/$(key)?ajax=true", "GET", null, headers)).get_object(); + var products = root.get_array_member("subproducts"); + + foreach(var product in products.get_elements()) + { + var game = new HumbleGame(this, key, product.get_object()); + if(!games.contains(game) && yield game.is_for_linux()) + { + games.add(game); + if(game_loaded != null) game_loaded(game); + GamesDB.get_instance().add_game(game); + } + games_count = games.size; + } + } + + return games; + } + } +} diff --git a/src/data/sources/humble/HumbleGame.vala b/src/data/sources/humble/HumbleGame.vala new file mode 100644 index 00000000..7cd065c8 --- /dev/null +++ b/src/data/sources/humble/HumbleGame.vala @@ -0,0 +1,225 @@ +using Gtk; +using Gee; +using GameHub.Utils; + +namespace GameHub.Data.Sources.Humble +{ + public class HumbleGame: Game + { + private bool _is_for_linux; + + private string order_id; + public File executable { get; private set; } + + private string installation_dir_name + { + owned get + { + return name.escape().replace(" ", "_").replace(":", ""); + } + } + + public HumbleGame(Humble src, string order, Json.Object json) + { + source = src; + id = json.get_string_member("machine_name"); + name = json.get_string_member("human_name"); + image = json.get_string_member("icon"); + icon = image; + order_id = order; + + _is_for_linux = false; + + foreach(var dl in json.get_array_member("downloads").get_elements()) + { + if(dl.get_object().get_string_member("platform") == "linux") + { + _is_for_linux = true; + break; + } + } + + if(!_is_for_linux) + { + GamesDB.get_instance().add_unsupported_game(source, id); + return; + } + + executable = FSUtils.file(FSUtils.Paths.Humble.Games, installation_dir_name + "/start.sh"); + + custom_info = @"{\"order\":\"$(order_id)\",\"executable\":\"$(executable.get_path())\"}"; + } + + public HumbleGame.from_db(Humble src, Sqlite.Statement stmt) + { + source = src; + id = stmt.column_text(1); + name = stmt.column_text(2); + icon = stmt.column_text(3); + image = stmt.column_text(4); + custom_info = stmt.column_text(5); + _is_for_linux = true; + + var custom_json = Parser.parse_json(custom_info).get_object(); + order_id = custom_json.get_string_member("order"); + executable = FSUtils.file(custom_json.get_string_member("executable")); + } + + public override async bool is_for_linux() + { + return _is_for_linux; + } + + public override bool is_installed() + { + return executable.query_exists(); + } + + public override async void install(DownloadProgress progress = (d, t) => {}) + { + var token = ((Humble) source).user_token; + + var headers = new HashMap(); + headers["Cookie"] = @"$(Humble.AUTH_COOKIE)=\"$(token)\";"; + + var root = (yield Parser.parse_remote_json_file_async(@"https://www.humblebundle.com/api/v1/order/$(order_id)?ajax=true", "GET", null, headers)).get_object(); + var products = root.get_array_member("subproducts"); + + if(products == null) return; + + var installers = new ArrayList(); + + foreach(var product_node in products.get_elements()) + { + var product = product_node.get_object(); + if(product.get_string_member("machine_name") != id) continue; + + foreach(var dl_node in product.get_array_member("downloads").get_elements()) + { + var dl = dl_node.get_object(); + var id = dl.get_string_member("machine_name"); + var os = dl.get_string_member("platform"); + if(os != "linux") continue; + + foreach(var dls_node in dl.get_array_member("download_struct").get_elements()) + { + var installer = new Installer(id, os, dls_node.get_object()); + installers.add(installer); + } + } + } + + if(installers.size < 1) return; + + var wnd = new GameHub.UI.Dialogs.GameInstallDialog(this, installers); + + wnd.canceled.connect(() => Idle.add(install.callback)); + + wnd.install.connect(installer => { + var link = installer.file; + var local = FSUtils.expand(FSUtils.Paths.Humble.Installers, "humble_" + installer.id); + + FSUtils.mkdir(FSUtils.Paths.Humble.Games); + FSUtils.mkdir(FSUtils.Paths.Humble.Installers); + + Downloader.get_instance().download.begin(File.new_for_uri(link), { local }, progress, null, (obj, res) => { + try + { + var file = Downloader.get_instance().download.end(res); + var path = file.get_path(); + var install_dir = FSUtils.expand(FSUtils.Paths.Humble.Games, installation_dir_name); + FSUtils.mkdir(install_dir); + Utils.run(@"chmod +x \"$(path)\""); + + var info = file.query_info(FileAttribute.STANDARD_CONTENT_TYPE, FileQueryInfoFlags.NONE); + var type = info.get_content_type(); + + var cmd = @"/usr/bin/env xdg-open $(path)"; // unknown type, just open + + switch(type) + { + case "application/x-executable": + case "application/x-elf": + case "application/x-sh": + case "application/x-shellscript": + cmd = @"$(path) -- --i-agree-to-all-licenses --noreadme --nooptions --noprompt --destination $(install_dir)"; // probably mojosetup + break; + + case "application/zip": + case "application/x-tar": + case "application/x-gtar": + case "application/x-cpio": + case "application/x-bzip2": + case "application/gzip": + case "application/x-lzip": + case "application/x-lzma": + case "application/x-7z-compressed": + case "application/x-rar-compressed": + cmd = @"/usr/bin/env file-roller $(path) -e $(install_dir)"; // extract with file-roller + break; + } + + Utils.run_async.begin(cmd, (obj, res) => { + Utils.run_async.end(res); + Utils.run(@"chmod -R +x \"$(install_dir)\""); + + var chooser = new FileChooserDialog(_("Select main executable of the game"), GameHub.UI.Windows.MainWindow.instance, + FileChooserAction.OPEN, _("Cancel"), ResponseType.CANCEL, _("Select"), ResponseType.ACCEPT); + var filter = new FileFilter(); + filter.add_mime_type("application/x-executable"); + filter.add_mime_type("application/x-elf"); + filter.add_mime_type("application/x-sh"); + filter.add_mime_type("text/x-shellscript"); + chooser.set_filter(filter); + chooser.select_file(executable); + + if(chooser.run() == ResponseType.ACCEPT) + { + executable = chooser.get_file(); + custom_info = @"{\"order\":\"$(order_id)\",\"executable\":\"$(executable.get_path())\"}"; + GamesDB.get_instance().add_game(this); + } + + chooser.destroy(); + + Idle.add(install.callback); + }); + } + catch(Error e) + { + warning(e.message); + } + }); + }); + + wnd.show_all(); + wnd.present(); + + yield; + } + + public override async void run() + { + if(is_installed()) + { + var path = executable.get_path(); + yield Utils.run_async(@"$(path)"); + } + } + + public class Installer: Game.Installer + { + public string dl_name; + + public override string name { get { return dl_name; } } + + public Installer(string machine_name, string platform, Json.Object download) + { + id = machine_name; + os = platform; + dl_name = download.get_string_member("name"); + file = download.get_object_member("url").get_string_member("web"); + } + } + } +} diff --git a/src/data/sources/steam/Steam.vala b/src/data/sources/steam/Steam.vala index 1f3d2b10..d1242c6d 100644 --- a/src/data/sources/steam/Steam.vala +++ b/src/data/sources/steam/Steam.vala @@ -43,7 +43,7 @@ namespace GameHub.Data.Sources.Steam var result = false; new Thread("steam-loginusers-thread", () => { - Json.Object config = Parser.parse_vdf_file(FSUtils.Paths.Steam.LoginUsersVDF); + var config = Parser.parse_vdf_file(FSUtils.Paths.Steam.LoginUsersVDF); var users = Parser.json_object(config, {"users"}); if(users == null) @@ -104,11 +104,12 @@ namespace GameHub.Data.Sources.Steam } } } + games_count = games.size; var url = @"https://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=$(api_key)&steamid=$(user_id)&format=json&include_appinfo=1&include_played_free_games=1"; var root = yield Parser.parse_remote_json_file_async(url); - var json_games = root.get_object_member("response").get_array_member("games"); + var json_games = Parser.json_object(root, {"response"}).get_array_member("games"); foreach(var g in json_games.get_elements()) { diff --git a/src/data/sources/steam/SteamGame.vala b/src/data/sources/steam/SteamGame.vala index f894b50b..c3921f37 100644 --- a/src/data/sources/steam/SteamGame.vala +++ b/src/data/sources/steam/SteamGame.vala @@ -16,9 +16,6 @@ namespace GameHub.Data.Sources.Steam var icon_hash = json.get_string_member("img_icon_url"); icon = @"https://media.steampowered.com/steamcommunity/public/images/apps/$(id)/$(icon_hash).jpg"; image = @"https://cdn.akamai.steamstatic.com/steam/apps/$(id)/header.jpg"; - int64 minutes = json.get_int_member("playtime_forever"); - int64 hours = (int) (minutes / 60.0f); - playtime = minutes > 60 ? @"$(hours) hours" : @"$(minutes) minutes"; if(GamesDB.get_instance().is_game_unsupported(src, id)) { @@ -33,7 +30,7 @@ namespace GameHub.Data.Sources.Steam name = stmt.column_text(2); icon = stmt.column_text(3); image = stmt.column_text(4); - playtime = stmt.column_text(5); + custom_info = stmt.column_text(5); _is_for_linux = true; } diff --git a/src/meson.build b/src/meson.build index 380d5b79..582de3fd 100644 --- a/src/meson.build +++ b/src/meson.build @@ -23,6 +23,9 @@ executable( 'data/sources/gog/GOG.vala', 'data/sources/gog/GOGGame.vala', + + 'data/sources/humble/Humble.vala', + 'data/sources/humble/HumbleGame.vala', 'data/GamesDB.vala', @@ -30,7 +33,7 @@ executable( 'ui/windows/WebAuthWindow.vala', 'ui/dialogs/SettingsDialog.vala', - 'ui/dialogs/GOGGameInstallDialog.vala', + 'ui/dialogs/GameInstallDialog.vala', 'ui/views/BaseView.vala', 'ui/views/WelcomeView.vala', diff --git a/src/ui/dialogs/GOGGameInstallDialog.vala b/src/ui/dialogs/GameInstallDialog.vala similarity index 63% rename from src/ui/dialogs/GOGGameInstallDialog.vala rename to src/ui/dialogs/GameInstallDialog.vala index 302f1020..6f291906 100644 --- a/src/ui/dialogs/GOGGameInstallDialog.vala +++ b/src/ui/dialogs/GameInstallDialog.vala @@ -5,19 +5,20 @@ using GameHub.Utils; using GameHub.Data; using GameHub.Data.Sources.GOG; +using GameHub.Data.Sources.Humble; namespace GameHub.UI.Dialogs { - public class GOGGameInstallDialog: Granite.MessageDialog + public class GameInstallDialog: Granite.MessageDialog { - public signal void install(GOGGame.Installer installer); + public signal void install(Game.Installer installer); public signal void canceled(); - private ListBox languages_list; + private ListBox installers_list; private bool is_finished = false; - public GOGGameInstallDialog(GOGGame game, ArrayList installers) + public GameInstallDialog(Game game, ArrayList installers) { Object(transient_for: Windows.MainWindow.instance, deletable: false, resizable: false); @@ -27,25 +28,25 @@ namespace GameHub.UI.Dialogs primary_text = game.name; - languages_list = new ListBox(); + installers_list = new ListBox(); var sys_langs = Intl.get_language_names(); foreach(var installer in installers) { - var row = new LangRow(installer); - languages_list.add(row); + var row = new InstallerRow(installer); + installers_list.add(row); - if(installer.lang in sys_langs) + if(installer is GOGGame.Installer && (installer as GOGGame.Installer).lang in sys_langs) { - languages_list.select_row(row); + installers_list.select_row(row); } } if(installers.size > 1) { - secondary_text = _("Select game language"); - custom_bin.child = languages_list; + secondary_text = _("Select game installer"); + custom_bin.child = installers_list; } destroy.connect(() => { if(!is_finished) canceled(); }); @@ -61,7 +62,7 @@ namespace GameHub.UI.Dialogs var installer = installers[0]; if(installers.size > 1) { - var row = languages_list.get_selected_row() as LangRow; + var row = installers_list.get_selected_row() as InstallerRow; installer = row.installer; } is_finished = true; @@ -79,15 +80,15 @@ namespace GameHub.UI.Dialogs show_all(); } - private class LangRow: ListBoxRow + private class InstallerRow: ListBoxRow { - public GOGGame.Installer installer; + public Game.Installer installer; - public LangRow(GOGGame.Installer installer) + public InstallerRow(Game.Installer installer) { this.installer = installer; - var label = new Label(installer.lang_full); + var label = new Label(installer.name); label.xpad = 16; label.ypad = 4; child = label; diff --git a/src/ui/dialogs/SettingsDialog.vala b/src/ui/dialogs/SettingsDialog.vala index a1a11f85..5fc0f9fe 100644 --- a/src/ui/dialogs/SettingsDialog.vala +++ b/src/ui/dialogs/SettingsDialog.vala @@ -35,6 +35,10 @@ namespace GameHub.UI.Dialogs add_header("GOG"); add_file_chooser(_("GOG games directory"), FileChooserAction.SELECT_FOLDER, paths.gog_games, v => { paths.gog_games = v; }); + add_separator(); + + add_header("Humble Bundle"); + add_file_chooser(_("Humble Bundle games directory"), FileChooserAction.SELECT_FOLDER, paths.humble_games, v => { paths.humble_games = v; }); content.pack_start(box, false, false, 0); diff --git a/src/ui/windows/WebAuthWindow.vala b/src/ui/windows/WebAuthWindow.vala index ad23eb1c..d8f7c53c 100644 --- a/src/ui/windows/WebAuthWindow.vala +++ b/src/ui/windows/WebAuthWindow.vala @@ -1,6 +1,7 @@ using Gtk; using GLib; using WebKit; +using Soup; using GameHub.Utils; namespace GameHub.UI.Windows @@ -14,7 +15,7 @@ namespace GameHub.UI.Windows public signal void finished(string url); public signal void canceled(); - public WebAuthWindow(string source, string url, string success_url_prefix) + public WebAuthWindow(string source, string url, string? success_url_prefix, string? success_cookie_name=null) { Object(transient_for: Windows.MainWindow.instance); @@ -30,8 +31,8 @@ namespace GameHub.UI.Windows webview = new WebView(); - var cookies = FSUtils.expand(FSUtils.Paths.Cache.Cookies); - webview.web_context.get_cookie_manager().set_persistent_storage(cookies, CookiePersistentStorage.TEXT); + var cookies_file = FSUtils.expand(FSUtils.Paths.Cache.Cookies); + webview.web_context.get_cookie_manager().set_persistent_storage(cookies_file, CookiePersistentStorage.TEXT); webview.get_settings().enable_mediasource = true; webview.get_settings().enable_smooth_scrolling = true; @@ -40,8 +41,26 @@ namespace GameHub.UI.Windows var uri = webview.get_uri(); titlebar.title = webview.title; titlebar.subtitle = uri.split("?")[0]; + titlebar.tooltip_text = uri; - if(uri.has_prefix(success_url_prefix)) + if(!is_finished && success_cookie_name != null) + { + webview.web_context.get_cookie_manager().get_cookies.begin(uri, null, (obj, res) => { + var cookies = webview.web_context.get_cookie_manager().get_cookies.end(res); + foreach(var cookie in cookies) + { + if(!is_finished && cookie.name == success_cookie_name && !cookie.value.contains("\"")) + { + is_finished = true; + finished(cookie.value); + destroy(); + break; + } + } + }); + } + + if(!is_finished && success_url_prefix != null && uri.has_prefix(success_url_prefix)) { is_finished = true; finished(uri.substring(success_url_prefix.length)); diff --git a/src/utils/FSUtils.vala b/src/utils/FSUtils.vala index 675cdc5d..177bfcfa 100644 --- a/src/utils/FSUtils.vala +++ b/src/utils/FSUtils.vala @@ -12,6 +12,7 @@ namespace GameHub.Utils { public string steam_home { get; set; } public string gog_games { get; set; } + public string humble_games { get; set; } public Settings() { @@ -54,6 +55,12 @@ namespace GameHub.Utils public static string Games { get { return FSUtils.Paths.Settings.get_instance().gog_games; } } public static string Installers { owned get { return FSUtils.Paths.GOG.Games + "/.installers"; } } } + + public class Humble + { + public static string Games { get { return FSUtils.Paths.Settings.get_instance().humble_games; } } + public static string Installers { owned get { return FSUtils.Paths.Humble.Games + "/.installers"; } } + } } public static string expand(string path, string file="") @@ -86,8 +93,10 @@ namespace GameHub.Utils mkdir(FSUtils.Paths.Cache.Home); mkdir(FSUtils.Paths.Cache.Images); - var cached_installers = FSUtils.expand(FSUtils.Paths.GOG.Installers, "{gog_*.sh~,.goutputstream-*}"); - Utils.run(@"bash -c 'rm $(cached_installers)'"); + var cache = FSUtils.expand(FSUtils.Paths.GOG.Installers, "{*~,.goutputstream-*}"); + Utils.run(@"bash -c 'rm $(cache)'"); + cache = FSUtils.expand(FSUtils.Paths.Humble.Installers, "{*~,.goutputstream-*}"); + Utils.run(@"bash -c 'rm $(cache)'"); } public static Pixbuf? get_icon(string name, int size=48) diff --git a/src/utils/Parser.vala b/src/utils/Parser.vala index 108caaf3..dbe57095 100644 --- a/src/utils/Parser.vala +++ b/src/utils/Parser.vala @@ -22,32 +22,41 @@ namespace GameHub.Utils return data; } - private static string load_remote_file(string url, string method="GET", string? auth = null) + private static Message prepare_message(string url, string method="GET", string? auth = null, HashMap? headers = null) { - var session = new Session(); var message = new Message(method, url); if(auth != null) { - var h = @"Bearer $(auth)"; - message.request_headers.append("Authorization", h); + message.request_headers.append("Authorization", "Bearer " + auth); + } + + if(headers != null) + { + foreach(var header in headers.entries) + { + message.request_headers.append(header.key, header.value); + } } + return message; + } + + private static string load_remote_file(string url, string method="GET", string? auth = null, HashMap? headers = null) + { + var session = new Session(); + var message = prepare_message(url, method, auth, headers); + var status = session.send_message(message); if (status == 200) return (string) message.response_body.data; return ""; } - private static async string load_remote_file_async(string url, string method="GET", string? auth = null) + private static async string load_remote_file_async(string url, string method="GET", string? auth = null, HashMap? headers = null) { var result = ""; var session = new Session(); - var message = new Message(method, url); - - if(auth != null) - { - message.request_headers.append("Authorization", "Bearer " + auth); - } + var message = prepare_message(url, method, auth, headers); session.queue_message(message, (s, m) => { if(m.status_code == 200) result = (string) m.response_body.data; @@ -57,59 +66,59 @@ namespace GameHub.Utils return result; } - public static Json.Object parse_json(string json) + public static Json.Node parse_json(string json) { try { var parser = new Json.Parser(); parser.load_from_data(json); - return parser.get_root().get_object(); + return parser.get_root(); } catch(GLib.Error e) { warning(e.message); } - return new Json.Object(); + return new Json.Node(Json.NodeType.NULL); } - public static Json.Object parse_vdf(string vdf) + public static Json.Node parse_vdf(string vdf) { return parse_json(vdf_to_json(vdf)); } - public static Json.Object parse_json_file(string path, string file="") + public static Json.Node parse_json_file(string path, string file="") { return parse_json(load_file(path, file)); } - public static Json.Object parse_vdf_file(string path, string file="") + public static Json.Node parse_vdf_file(string path, string file="") { return parse_vdf(load_file(path, file)); } - public static Json.Object parse_remote_json_file(string url, string method="GET", string? auth = null) + public static Json.Node parse_remote_json_file(string url, string method="GET", string? auth = null, HashMap? headers = null) { - return parse_json(load_remote_file(url, method, auth)); + return parse_json(load_remote_file(url, method, auth, headers)); } - public static Json.Object parse_remote_vdf_file(string url, string method="GET", string? auth = null) + public static Json.Node parse_remote_vdf_file(string url, string method="GET", string? auth = null, HashMap? headers = null) { - return parse_vdf(load_remote_file(url, method, auth)); + return parse_vdf(load_remote_file(url, method, auth, headers)); } - public static async Json.Object parse_remote_json_file_async(string url, string method="GET", string? auth = null) + public static async Json.Node parse_remote_json_file_async(string url, string method="GET", string? auth = null, HashMap? headers = null) { - return parse_json(yield load_remote_file_async(url, method, auth)); + return parse_json(yield load_remote_file_async(url, method, auth, headers)); } - public static async Json.Object parse_remote_vdf_file_async(string url, string method="GET", string? auth = null) + public static async Json.Node parse_remote_vdf_file_async(string url, string method="GET", string? auth = null, HashMap? headers = null) { - return parse_vdf(yield load_remote_file_async(url, method, auth)); + return parse_vdf(yield load_remote_file_async(url, method, auth, headers)); } - public static Json.Object? json_object(Json.Object root, string[] keys) + public static Json.Object? json_object(Json.Node root, string[] keys) { - Json.Object? obj = root; + Json.Object? obj = root.get_object(); foreach(var key in keys) { diff --git a/src/utils/Settings.vala b/src/utils/Settings.vala index 1e38d3ed..025a2dc5 100644 --- a/src/utils/Settings.vala +++ b/src/utils/Settings.vala @@ -99,5 +99,26 @@ namespace GameHub.Settings return instance; } } + + public class Humble: Granite.Services.Settings + { + public bool authenticated { get; set; } + public string access_token { get; set; } + + public Humble() + { + base(ProjectConfig.PROJECT_NAME + ".auth.humble"); + } + + private static Humble? instance; + public static unowned Humble get_instance() + { + if(instance == null) + { + instance = new Humble(); + } + return instance; + } + } } }