tkashkin / GameHub

All your games in one place
https://tkashkin.github.io/projects/gamehub
GNU General Public License v3.0
2.23k stars 128 forks source link

[Feature Request] IndieGala freebies #333

Open IanTrudel opened 4 years ago

IanTrudel commented 4 years ago

https://www.indiegala.com/

IndieGala gives away free games on a regular basis. Integration to GameHub would certainly make things easier.

Lucki commented 4 years ago

Is there an API? Their freebie game detail page looks somewhat like itch.io.

URLs and all other information are directly in the HTML: https://www.indiegala.com/library/showcase/1

HTML for one item (sadly I don't have freebies with executable besides windows): ~~~ html ~~~
HTML to the last site: ~~~ html ~~~

Edit: Incapsula blocks everything.

Protektor-Desura commented 3 years ago

I would be interested as well especially since they offer Linux versions of games as well.

Protektor-Desura commented 3 years ago

You could look at this code to see how they are doing things as to how to integrate IndieGala in to GameHub.

https://github.com/burnhamup/galaxy-integration-indiegala

Lucki commented 3 years ago

This one is actually interesting: https://github.com/burnhamup/galaxy-integration-indiegala/commit/9dad774aa9e4bde16233cd5c320e207cd2960d04

Lucki commented 3 years ago

Using the API does circumvent the blocking from incapsula.

Here's a rudimentary non functional diff to the refactoring branch to get someone going:

diff --git a/data/com.github.tkashkin.gamehub.gschema.xml.in b/data/com.github.tkashkin.gamehub.gschema.xml.in
index 667b85f..a84d30a 100644
--- a/data/com.github.tkashkin.gamehub.gschema.xml.in
+++ b/data/com.github.tkashkin.gamehub.gschema.xml.in
@@ -192,6 +192,17 @@
            </key>
        </schema>

+       <schema path="@SCHEMA_PATH@/auth/indiegala/" id="@SCHEMA_ID@.auth.indiegala">
+           <key name="enabled" type="b">
+               <default>true</default>
+               <summary>Is IndieGala enabled</summary>
+           </key>
+           <key name="authenticated" type="b">
+               <default>false</default>
+               <summary>Is user authenticated</summary>
+           </key>
+       </schema>
+
        <schema path="@SCHEMA_PATH@/auth/itch/" id="@SCHEMA_ID@.auth.itch">
            <key name="enabled" type="b">
                <default>true</default>
@@ -239,6 +250,18 @@
            </key>
        </schema>

+   <!-- Paths / IndieGala -->
+       <schema path="@SCHEMA_PATH@/paths/indiegala/" id="@SCHEMA_ID@.paths.indiegala">
+           <key name="game-directories" type="as">
+               <default>['~/Games/IndieGala']</default>
+               <summary>IndieGala game directories</summary>
+           </key>
+           <key name="default-game-directory" type="s">
+               <default>'~/Games/IndieGala'</default>
+               <summary>Default IndieGala games directory</summary>
+           </key>
+       </schema>
+
    <!-- Paths / itch.io -->
        <schema path="@SCHEMA_PATH@/paths/itch/" id="@SCHEMA_ID@.paths.itch">
            <key name="home" type="s">
@@ -293,6 +316,17 @@
            </key>
        </schema>

+       <schema path="@SCHEMA_PATH@/paths/collection/indiegala/" id="@SCHEMA_ID@.paths.collection.indiegala">
+           <key name="game-dir" type="s">
+               <default>'$root/IndieGala/$game'</default>
+               <summary>IndieGala collection: game directory</summary>
+           </key>
+           <key name="installers" type="s">
+               <default>'$game_dir'</default>
+               <summary>IndieGala collection: installers directory</summary>
+           </key>
+       </schema>
+
    <!-- Controller -->
        <schema path="@SCHEMA_PATH@/controller/" id="@SCHEMA_ID@.controller">
            <key name="enabled" type="b">
diff --git a/src/app.vala b/src/app.vala
index 7b0ed93..3dcd818 100644
--- a/src/app.vala
+++ b/src/app.vala
@@ -25,6 +25,7 @@ using GameHub.Data.DB;
 using GameHub.Data.Sources.Steam;
 using GameHub.Data.Sources.GOG;
 using GameHub.Data.Sources.Humble;
+using GameHub.Data.Sources.IndieGala;
 using GameHub.Data.Sources.Itch;
 using GameHub.Data.Sources.User;
 using GameHub.Data.Tweaks;
@@ -141,7 +142,7 @@ namespace GameHub
            ImageCache.init();
            Database.create();

-           GameSources = { new Steam(), new GOG(), new Humble(), new Trove(), new Itch(), new User() };
+           GameSources = { new Steam(), new GOG(), new Humble(), new Trove(), new IndieGala(), new Itch(), new User() };

            Providers.ImageProviders = { new Providers.Images.Steam(), new Providers.Images.SteamGridDB(), new Providers.Images.JinxSGVI() };
            Providers.DataProviders  = { new Providers.Data.IGDB() };
diff --git a/src/data/db/tables/Games.vala b/src/data/db/tables/Games.vala
index 38b6c99..79c2acd 100644
--- a/src/data/db/tables/Games.vala
+++ b/src/data/db/tables/Games.vala
@@ -25,6 +25,7 @@ using GameHub.Data.Runnables;
 using GameHub.Data.Sources.Steam;
 using GameHub.Data.Sources.GOG;
 using GameHub.Data.Sources.Humble;
+using GameHub.Data.Sources.IndieGala;
 using GameHub.Data.Sources.Itch;
 using GameHub.Data.Sources.User;

@@ -351,6 +352,10 @@ namespace GameHub.Data.DB.Tables
                {
                    g = new HumbleGame.from_db((Humble) s, st);
                }
+               else if(s is IndieGala)
+               {
+                   g = new IndieGalaGame.from_db((IndieGala) s, st);
+               }
                else if(s is Itch)
                {
                    g = new ItchGame.from_db((Itch) s, st);
@@ -436,6 +441,10 @@ namespace GameHub.Data.DB.Tables
                    {
                        g = new HumbleGame.from_db((Humble) s, st);
                    }
+                   else if(s is IndieGala)
+                   {
+                       g = new IndieGalaGame.from_db((IndieGala) s, st);
+                   }
                    else if(s is Itch)
                    {
                        g = new ItchGame.from_db((Itch) s, st);
diff --git a/src/data/sources/indiegala/IndieGala.vala b/src/data/sources/indiegala/IndieGala.vala
new file mode 100644
index 0000000..9531557
--- /dev/null
+++ b/src/data/sources/indiegala/IndieGala.vala
@@ -0,0 +1,320 @@
+using Gee;
+using Soup;
+
+using GameHub.Data.DB;
+using GameHub.Data.Runnables;
+using GameHub.Utils;
+
+namespace GameHub.Data.Sources.IndieGala
+{
+   public class IndieGala: GameSource
+   {
+       public static IndieGala instance;
+
+       public override string id { get { return "indiegala"; } }
+       public override string name { get { return "IndieGala"; } }
+       public override string icon { get { return "source-gog-symbolic"; } }
+
+       public override bool enabled
+       {
+           get { return settings.enabled; }
+           set { settings.enabled = value; }
+       }
+
+       public string? user_id { get; protected set; }
+       public string? user_name { get; protected set; }
+
+       SList<Soup.Cookie>? cookies = null;
+
+       private Settings.Auth.IndieGala settings;
+
+       public IndieGala()
+       {
+           instance = this;
+
+           settings = Settings.Auth.IndieGala.instance;
+       }
+
+       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_cookies();
+       }
+
+       public override bool is_authenticated()
+       {
+           return cookies != null;
+       }
+
+       public override bool can_authenticate_automatically()
+       {
+           return false;
+       }
+
+       private async bool get_cookies()
+       {
+           //  if(cookies != null)
+           //  {
+           //      return true;
+           //  }
+
+           var wnd = new GameHub.UI.Windows.WebAuthWindow(this.name, @"https://www.indiegala.com/login", null, "auth");
+
+           wnd.finished.connect(code =>
+           {
+               //  user_auth_code = code;
+               //  if(GameHub.Application.log_auth)
+               //  {
+               //      debug("[Auth] IndieGala auth code: %s", code);
+               //  }
+               //  Idle.add(get_auth_code.callback);
+
+               wnd.webview.web_context.get_cookie_manager().get_cookies.begin(
+                   "https://www.indiegala.com",
+                   null,
+                   (obj, res) => {
+                   try
+                   {
+                       var webview_cookies = wnd.webview.web_context.get_cookie_manager().get_cookies.end(res);
+                       cookies             = new SList<Soup.Cookie>();
+
+                       webview_cookies.foreach(cookie => {
+                           cookies.append(cookie);
+                       });
+
+                       //  authenticate_with_exchange_code(authenticate_with_sid(cookies));
+                   }
+                   catch (Error e) {}
+
+                   Idle.add(get_cookies.callback);
+               });
+           });
+
+           wnd.canceled.connect(() => Idle.add(get_cookies.callback));
+
+           wnd.set_size_request(550, 680);
+           wnd.show_all();
+           wnd.present();
+
+           yield;
+
+           return cookies != null;
+       }
+
+       //  private async bool get_token()
+       //  {
+       //      if(user_token != null)
+       //      {
+       //          return true;
+       //      }
+
+       //      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)).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");
+
+       //      settings.access_token = user_token ?? "";
+       //      settings.refresh_token = user_refresh_token ?? "";
+
+       //      if(GameHub.Application.log_auth)
+       //      {
+       //          debug("[Auth] GOG access token: %s", user_token);
+       //          debug("[Auth] GOG refresh token: %s", user_refresh_token);
+       //          debug("[Auth] GOG user id: %s", user_id);
+       //      }
+
+       //      return user_token != null;
+       //  }
+
+       //  public async bool refresh_token()
+       //  {
+       //      if(user_refresh_token == null)
+       //      {
+       //          return false;
+       //      }
+
+       //      if(GameHub.Application.log_auth)
+       //      {
+       //          debug("[Auth] Refreshing GOG access token with refresh token: %s", user_refresh_token);
+       //      }
+
+       //      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_node = yield Parser.parse_remote_json_file_async(url);
+       //      var root = root_node != null && root_node.get_node_type() == Json.NodeType.OBJECT ? root_node.get_object() : null;
+
+       //      if(root == null)
+       //      {
+       //          token_needs_refresh = false;
+       //          return false;
+       //      }
+
+       //      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");
+
+       //      settings.access_token = user_token ?? "";
+       //      settings.refresh_token = user_refresh_token ?? "";
+
+       //      if(GameHub.Application.log_auth)
+       //      {
+       //          debug("[Auth] GOG access token: %s", user_token);
+       //          debug("[Auth] GOG refresh token: %s", user_refresh_token);
+       //          debug("[Auth] GOG user id: %s", user_id);
+       //      }
+
+       //      token_needs_refresh = false;
+
+       //      return user_token != null;
+       //  }
+
+       private ArrayList<Game> _games = new ArrayList<Game>(Game.is_equal);
+
+       public override ArrayList<Game> games { get { return _games; } }
+
+       public override async ArrayList<Game> load_games(Utils.FutureResult2<Game, bool>? game_loaded=null, Utils.Future? cache_loaded=null)
+       {
+           if(_games.size > 0)
+           {
+               return _games;
+           }
+
+           Utils.thread("IndieGalaLoading", () => {
+               _games.clear();
+
+               var cached = Tables.Games.get_all(this);
+               games_count = 0;
+               if(cached.size > 0)
+               {
+                   foreach(var g in cached)
+                   {
+                       if(!Settings.UI.Behavior.instance.merge_games || !Tables.Merges.is_game_merged(g))
+                       {
+                           _games.add(g);
+                           if(game_loaded != null)
+                           {
+                               game_loaded(g, true);
+                           }
+                       }
+                       games_count++;
+                   }
+               }
+
+               if(cache_loaded != null)
+               {
+                   cache_loaded();
+               }
+
+               var session = new Session();
+               session.timeout            = 5;
+               session.max_conns          = 256;
+               session.max_conns_per_host = 256;
+
+               var message = new Message("GET", "https://www.indiegala.com/login_new/user_info");
+               message.request_headers.append("User-Agent", "galaClient");
+               cookies_to_request(cookies, message);
+               var status = session.send_message(message);
+               debug("[Sources.IndieGala.load_games] Status: %s", status.to_string());
+               assert(status <= 400);
+               cookies = cookies_from_response(message);
+
+               var json = Parser.parse_json((string) message.response_body.data);
+               debug(Json.to_string(json, true));
+               assert(json.get_node_type() == Json.NodeType.OBJECT);
+               assert(json.get_object().has_member("status") && json.get_object().get_string_member("status") == "success");
+               assert(json.get_object().has_member("showcase_content"));
+               assert(json.get_object().get_object_member("showcase_content").has_member("status_code_ok") && json.get_object().get_object_member("showcase_content").get_boolean_member("status_code_ok") == true);
+               assert(json.get_object().get_object_member("showcase_content").has_member("content"));
+               assert(json.get_object().get_object_member("showcase_content").get_object_member("content").has_member("user_collection"));
+
+               json.get_object().get_object_member("showcase_content").get_object_member("content").get_array_member("user_collection").foreach_element((array, index, node) =>
+               {
+                   var game = new IndieGalaGame(this, node);
+                   bool is_new_game = !_games.contains(game);
+
+                   if(is_new_game && (!Settings.UI.Behavior.instance.merge_games || !Tables.Merges.is_game_merged(game)))
+                   {
+                       _games.add(game);
+
+                       if(game_loaded != null)
+                       {
+                           game_loaded(game, false);
+                       }
+                   }
+
+                   if(is_new_game)
+                   {
+                       games_count++;
+                       game.save();
+                   }
+               });
+
+               Idle.add(load_games.callback);
+           });
+
+           yield;
+
+           return _games;
+       }
+
+       public override ArrayList<File>? game_dirs
+       {
+           owned get
+           {
+               ArrayList<File>? dirs = null;
+
+               var paths = GameHub.Settings.Paths.IndieGala.instance.game_directories;
+               if(paths != null && paths.length > 0)
+               {
+                   foreach(var path in paths)
+                   {
+                       if(path != null && path.length > 0)
+                       {
+                           var dir = FS.file(path);
+                           if(dir != null)
+                           {
+                               if(dirs == null) dirs = new ArrayList<File>();
+                               dirs.add(dir);
+                           }
+                       }
+                   }
+               }
+
+               return dirs;
+           }
+       }
+
+       public override File? default_game_dir
+       {
+           owned get
+           {
+               var path = GameHub.Settings.Paths.IndieGala.instance.default_game_directory;
+               if(path != null && path.length > 0)
+               {
+                   var dir = FS.file(path);
+                   if(dir != null && dir.query_exists())
+                   {
+                       return dir;
+                   }
+               }
+               var dirs = game_dirs;
+               if(dirs != null && dirs.size > 0)
+               {
+                   return dirs.first();
+               }
+               return null;
+           }
+       }
+   }
+}
diff --git a/src/data/sources/indiegala/IndieGalaGame.vala b/src/data/sources/indiegala/IndieGalaGame.vala
new file mode 100644
index 0000000..ed83441
--- /dev/null
+++ b/src/data/sources/indiegala/IndieGalaGame.vala
@@ -0,0 +1,489 @@
+using Gee;
+
+using GameHub.Data.DB;
+using GameHub.Data.Runnables;
+using GameHub.Data.Runnables.Tasks.Install;
+using GameHub.Data.Tweaks;
+
+using GameHub.Utils;
+using GameHub.Utils.FS;
+
+namespace GameHub.Data.Sources.IndieGala
+{
+   public class IndieGalaGame: Game,
+       Traits.HasActions, Traits.HasExecutableFile, Traits.SupportsCompatTools,
+       Traits.Game.SupportsOverlays, Traits.Game.SupportsTweaks
+   {
+       // Traits.HasActions
+       public override ArrayList<Traits.HasActions.Action>? actions { get; protected set; default = new ArrayList<Traits.HasActions.Action>(); }
+
+       // Traits.HasExecutableFile
+       public override string? executable_path { owned get; set; }
+       public override string? work_dir_path { owned get; set; }
+       public override string? arguments { owned get; set; }
+       public override string? environment { owned get; set; }
+
+       // Traits.SupportsCompatTools
+       public override string? compat_tool { get; set; }
+       public override string? compat_tool_settings { get; set; }
+
+       public bool supports_compat_tools { get { return cast<Traits.SupportsCompatTools>().supports_compat_tools; } }
+       public bool needs_compat { get { return cast<Traits.SupportsCompatTools>().needs_compat; } }
+       public bool force_compat
+       {
+           get { return cast<Traits.SupportsCompatTools>().force_compat; }
+           set { cast<Traits.SupportsCompatTools>().force_compat = value; }
+       }
+       public bool use_compat { get { return cast<Traits.SupportsCompatTools>().use_compat; } }
+
+       // Traits.Game.SupportsOverlays
+       public override ArrayList<Traits.Game.SupportsOverlays.Overlay> overlays { get; set; default = new ArrayList<Traits.Game.SupportsOverlays.Overlay>(); }
+       protected override FSOverlay? fs_overlay { get; set; }
+       protected override string? fs_overlay_last_options { get; set; }
+
+       // Traits.Game.SupportsTweaks
+       public override TweakSet? tweaks { get; set; default = null; }
+
+       public bool has_updates { get; set; default = false; }
+
+       private bool game_info_updating = false;
+       private bool game_info_updated = false;
+
+       private string? prod_slugged_name = null;
+
+       public IndieGalaGame(IndieGala src, Json.Node json_node)
+       {
+           source = src;
+
+           var json_obj = json_node.get_object();
+
+           id = json_obj.get_string_member("prod_id_key_name");
+           name = json_obj.get_string_member("prod_name");
+           icon = "";
+           info = Json.to_string(json_node, false);
+
+           if(json_obj.has_member("prod_dev_image"))
+           {
+                //  https://www.indiegalacdn.com/imgs/devs/freebies/products/50025bf2-7190-4a32-aa15-4e4f89ba8da6/prodcover/1615375739.png
+               //  image = "https://www.indiegalacdn.com/imgs/devs/"
+                //  + json_obj.get_string_member("prod_dev_namespace")
+                //  + "/products/"
+                //  + json_obj.get_string_member("prod_id_key_name")
+                //  + "/prodcover/"
+                //  + json_obj.get_string_member("prod_dev_cover");
+
+                //  https://www.indiegalacdn.com/imgs/devs/freebies/products/50025bf2-7190-4a32-aa15-4e4f89ba8da6/prodmain/1615375754.png
+                image = "https://www.indiegalacdn.com/imgs/devs/"
+                + json_obj.get_string_member("prod_dev_namespace")
+                + "/products/"
+                + json_obj.get_string_member("prod_id_key_name")
+                + "/prodmain/"
+                + json_obj.get_string_member("prod_dev_image");
+           }
+
+            json_obj.get_array_member("version").foreach_element((array, index, node) => {
+               if(node.get_object().get_int_member("enabled") == 0) return;
+
+               if(node.get_object().get_string_member("os") == "win") platforms.add(Platform.WINDOWS);
+               if(node.get_object().get_string_member("os") == "mac") platforms.add(Platform.MACOS);
+               if(node.get_object().get_string_member("os") == "lin") platforms.add(Platform.LINUX);
+           });
+
+           install_dir = null;
+           executable_path = "${install_dir}/start.sh";
+           work_dir_path = "${install_dir}";
+
+           init_tweaks();
+
+           mount_overlays.begin();
+           update_status();
+       }
+
+       public IndieGalaGame.from_db(IndieGala src, Sqlite.Statement s)
+       {
+           source = src;
+
+           dbinit(s);
+           dbinit_executable(s);
+           dbinit_compat(s);
+           dbinit_tweaks(s);
+
+           mount_overlays.begin();
+           update_status();
+       }
+
+       //  public override async void update_game_info()
+       //  {
+       //      if(game_info_updating) return;
+       //      game_info_updating = true;
+
+       //      yield remount_overlays();
+       //      update_status();
+
+       //      if(info_detailed == null || info_detailed.length == 0)
+       //      {
+       //          var lang = Intl.setlocale(LocaleCategory.ALL, null).down().substring(0, 2);
+       //          var url = @"https://api.gog.com/products/$(id)?expand=downloads,description,expanded_dlcs" + (lang != null && lang.length > 0 ? "&locale=" + lang : "");
+
+       //          while(true)
+       //          {
+       //              uint status = 0;
+       //              var json = (yield Parser.load_remote_file_async(url, "GET", ((GOG) source).user_token, null, null, out status));
+
+       //              if(status == Soup.Status.OK && json != null && json.length > 0)
+       //              {
+       //                  info_detailed = json;
+       //                  break;
+       //              }
+       //              else if(status == Soup.Status.UNAUTHORIZED)
+       //              {
+       //                  yield ((GOG) source).refresh_token();
+       //              }
+       //              else break;
+       //          }
+       //      }
+
+       //      var root = Parser.parse_json(info_detailed);
+
+       //      var images = Parser.json_object(root, {"images"});
+       //      var desc = Parser.json_object(root, {"description"});
+       //      var links = Parser.json_object(root, {"links"});
+
+       //      if(image == null || image == "")
+       //      {
+       //          var i = Parser.parse_json(info).get_object();
+       //          image = "https:" + i.get_string_member("image") + "_392.jpg";
+       //      }
+
+       //      if((icon == null || icon == "") && (images != null && images.has_member("icon")))
+       //      {
+       //          icon = images.get_string_member("icon");
+       //          if(icon != null) icon = "https:" + icon;
+       //          else icon = image;
+       //      }
+
+       //      is_installable = root != null && root.get_node_type() == Json.NodeType.OBJECT
+       //          && root.get_object().has_member("is_installable") && root.get_object().get_boolean_member("is_installable");
+
+       //      if(desc != null)
+       //      {
+       //          description = desc.get_string_member("full");
+       //          var cool = desc.get_string_member("whats_cool_about_it");
+       //          if(cool != null && cool.length > 0)
+       //          {
+       //              description += "<ul>";
+       //              var cool_parts = cool.split("\n");
+       //              foreach(var part in cool_parts)
+       //              {
+       //                  part = part.strip();
+       //                  if(part.length > 0)
+       //                  {
+       //                      description += "<li>" + part + "</li>";
+       //                  }
+       //              }
+       //              description += "</ul>";
+       //          }
+       //      }
+
+       //      if(links != null)
+       //      {
+       //          store_page = links.get_string_member("product_card");
+       //      }
+
+       //      var downloads = Parser.json_object(root, {"downloads"});
+
+       //      var installers_json = downloads == null || !downloads.has_member("installers") ? null : downloads.get_array_member("installers");
+       //      if(installers_json != null && (installers == null || installers.size == 0))
+       //      {
+       //          installers = new ArrayList<Runnables.Tasks.Install.Installer>();
+       //          foreach(var installer_json in installers_json.get_elements())
+       //          {
+       //              var installer = new Installer(this, installer_json.get_object());
+       //              installers.add(installer);
+       //          }
+       //      }
+
+       //      if(installers.size == 0)
+       //      {
+       //          is_installable = false;
+       //      }
+
+       //      var bonuses_json = downloads == null || !downloads.has_member("bonus_content") ? null : downloads.get_array_member("bonus_content");
+       //      if(bonuses_json != null && (bonus_content == null || bonus_content.size == 0))
+       //      {
+       //          bonus_content = new ArrayList<BonusContent>();
+       //          Json.Object? bonus_map = null;
+
+       //          if(bonus_content_dir != null && bonus_content_dir.query_exists())
+       //          {
+       //              var map_file = bonus_content_dir.get_child(BonusContent.FILEMAP_NAME);
+       //              if(map_file != null && map_file.query_exists())
+       //              {
+       //                  var map_root_node = Parser.parse_json_file(map_file.get_path());
+       //                  bonus_map = map_root_node != null && map_root_node.get_node_type() == Json.NodeType.OBJECT ? map_root_node.get_object() : null;
+       //              }
+       //          }
+
+       //          foreach(var bonus_json in bonuses_json.get_elements())
+       //          {
+       //              bonus_content.add(new BonusContent(this, bonus_json.get_object(), bonus_map));
+       //          }
+       //      }
+
+       //      var dlcs_json = root == null || root.get_node_type() != Json.NodeType.OBJECT || !root.get_object().has_member("expanded_dlcs") ? null : root.get_object().get_array_member("expanded_dlcs");
+       //      if(dlcs_json != null && (dlc == null || dlc.size == 0))
+       //      {
+       //          dlc = new ArrayList<DLC>();
+       //          foreach(var dlc_json in dlcs_json.get_elements())
+       //          {
+       //              var d = new DLC(this, dlc_json);
+       //              dlc.add(d);
+       //              yield d.update_downloads_info();
+       //          }
+       //      }
+
+       //      root = Parser.parse_json(info);
+
+       //      var tags_json = root == null || root.get_node_type() != Json.NodeType.OBJECT || !root.get_object().has_member("tags") ? null : root.get_object().get_array_member("tags");
+
+       //      if(tags_json != null)
+       //      {
+       //          foreach(var tag_json in tags_json.get_elements())
+       //          {
+       //              var tid = source.id + ":" + tag_json.get_string();
+       //              foreach(var t in Tables.Tags.TAGS)
+       //              {
+       //                  if(tid == t.id)
+       //                  {
+       //                      if(!tags.contains(t)) tags.add(t);
+       //                      break;
+       //                  }
+       //              }
+       //          }
+       //      }
+
+       //      save();
+
+       //      update_status();
+
+       //      game_info_updated = true;
+       //      game_info_updating = false;
+       //  }
+
+       public override async ArrayList<Tasks.Install.Installer>? load_installers()
+       {
+           if(installers != null && installers.size > 0) return installers;
+
+           if(info != null && (installers == null || installers.size == 0))
+           {
+               var installers_json = Parser.parse_json(info);
+               installers = new ArrayList<Runnables.Tasks.Install.Installer>();
+               foreach(var platform in platforms)
+               {
+                   var installer = new Installer(this, installers_json.get_object(), platform);
+                   installers.add(installer);
+               }
+           }
+
+           return installers;
+       }
+
+       public override async void run(){ yield run_executable(); }
+
+       //  public override async void uninstall()
+       //  {
+       //      if(install_dir != null && install_dir.query_exists())
+       //      {
+       //          string? uninstaller = null;
+       //          try
+       //          {
+       //              FileInfo? finfo = null;
+       //              var enumerator = yield install_dir.enumerate_children_async("standard::*", FileQueryInfoFlags.NONE);
+       //              while((finfo = enumerator.next_file()) != null)
+       //              {
+       //                  if(finfo.get_name().has_prefix("uninstall-"))
+       //                  {
+       //                      uninstaller = finfo.get_name();
+       //                      break;
+       //                  }
+       //              }
+       //          }
+       //          catch(Error e){}
+
+       //          yield umount_overlays();
+
+       //          if(uninstaller != null)
+       //          {
+       //              uninstaller = FS.expand(install_dir.get_path(), uninstaller);
+       //              debug("[GOGGame] Running uninstaller '%s'...", uninstaller);
+       //              yield Utils.exec({uninstaller, "--noprompt", "--force"}).override_runtime(true).sync_thread();
+       //          }
+       //          else
+       //          {
+       //              FS.rm(install_dir.get_path(), "", "-rf");
+       //          }
+       //          update_status();
+       //      }
+       //      if((install_dir == null || !install_dir.query_exists()) && (executable == null || !executable.query_exists()))
+       //      {
+       //          install_dir = null;
+       //          executable = null;
+       //          save();
+       //          update_status();
+       //      }
+       //  }
+
+       //  public override void update_status()
+       //  {
+       //      if(status.state == Game.State.DOWNLOADING && status.download.status.state != Downloader.Download.State.CANCELLED) return;
+
+       //      var state = Game.State.UNINSTALLED;
+
+       //      var gameinfo = get_file("gameinfo");
+       //      var goggame = get_file(@"goggame-$(id).info");
+       //      var gh_marker = get_file(@".gamehub_$(id)");
+
+       //      var files = new ArrayList<File>();
+
+       //      files.add(goggame);
+       //      files.add(gh_marker);
+
+       //      if(!(this is DLC))
+       //      {
+       //          files.add(executable);
+       //          files.add(gameinfo);
+       //      }
+
+       //      foreach(var file in files)
+       //      {
+       //          if(file != null && file.query_exists())
+       //          {
+       //              state = Game.State.INSTALLED;
+       //              break;
+       //          }
+       //      }
+
+       //      status = new Game.Status(state, this);
+       //      if(state == Game.State.INSTALLED)
+       //      {
+       //          remove_tag(Tables.Tags.BUILTIN_UNINSTALLED);
+       //          add_tag(Tables.Tags.BUILTIN_INSTALLED);
+       //      }
+       //      else
+       //      {
+       //          add_tag(Tables.Tags.BUILTIN_UNINSTALLED);
+       //          remove_tag(Tables.Tags.BUILTIN_INSTALLED);
+       //      }
+
+       //      if(gameinfo != null && gameinfo.query_exists())
+       //      {
+       //          try
+       //          {
+       //              string info;
+       //              FileUtils.get_contents(gameinfo.get_path(), out info);
+       //              var lines = info.split("\n");
+       //              if(lines.length >= 2)
+       //              {
+       //                  version = lines[1];
+       //              }
+       //          }
+       //          catch(Error e)
+       //          {
+       //              warning("[GOGGame.update_status] Error while reading gameinfo: %s", e.message);
+       //          }
+       //      }
+       //      else
+       //      {
+       //          load_version();
+       //      }
+
+       //      actions.clear();
+       //      if(goggame != null && goggame.query_exists())
+       //      {
+       //          var goggame_node = Parser.parse_json_file(goggame.get_path());
+       //          if(goggame_node != null && goggame_node.get_node_type() == Json.NodeType.OBJECT)
+       //          {
+       //              var goggame_obj = goggame_node.get_object();
+       //              var tasks = goggame_obj.has_member("playTasks") ? goggame_obj.get_array_member("playTasks") : null;
+       //              if(tasks != null)
+       //              {
+       //                  foreach(var task_node in tasks.get_elements())
+       //                  {
+       //                      if(task_node == null || task_node.get_node_type() != Json.NodeType.OBJECT) continue;
+       //                      var action = new RunnableAction(this, task_node.get_object());
+       //                      if(!action.is_hidden)
+       //                      {
+       //                          actions.add(action);
+       //                      }
+       //                  }
+       //              }
+       //          }
+       //      }
+       //  }
+
+       public class Installer: Runnables.Tasks.Install.DownloadableInstaller
+       {
+           public IndieGalaGame game { get; construct set; }
+           public Json.Object json { get; construct set; }
+           public Json.Object platform_json { get; construct set; }
+           private bool fetched = false;
+
+           public Installer(IndieGalaGame game, Json.Object json, Platform platform)
+           {
+               string g = game.name;
+               Json.Object j = null;
+
+               json.get_array_member("version").foreach_element((array, index, node) => {
+                   if(node.get_object().get_int_member("enabled") == 0) return;
+
+                   var os = node.get_object().get_string_member("os");
+                   if(platform == Platform.WINDOWS && os == "win") {
+                       j = node.get_object();
+                   } else if(platform == Platform.MACOS && os == "mac") {
+                       j = node.get_object();
+                   } else if(platform == Platform.LINUX && os == "lin") {
+                       j = node.get_object();
+                   }
+               });
+
+               assert(j != null);
+
+               Object(
+                   game: game,
+                   json: json,
+                   platform_json: j,
+                   id: j.get_string_member("id"),
+                   name: json.get_string_member("prod_name"),
+                   platform: platform,
+                   version: j.get_string_member("version"),
+                   installers_dir: FS.file(Settings.Paths.Collection.IndieGala.expand_installers(g, platform))
+               );
+           }
+
+           public override async void fetch_parts()
+           {
+               if(fetched || installers_dir == null) return;
+
+               //  https://content.indiegalacdn.com/dev_fold_freebies/50025bf2-7190-4a32-aa15-4e4f89ba8da6/win/way-to-go_win.zip
+               var url = "https://content.indiegalacdn.com/dev_fold_"
+               + json.get_string_member("prod_dev_namespace")
+               + "/"
+               + json.get_string_member("prod_id_key_name")
+               + "/"
+               + platform_json.get_string_member("os")
+               + "/"
+               + json.get_string_member("prod_slugged_name")
+               + "_"
+               + platform_json.get_string_member("os")
+               + ".zip";
+
+
+               var remote = File.new_for_uri(url);
+               var local = installers_dir.get_child("indiegala_" + game.id + "_" + this.id);
+               parts.add(new Part(id, url, 0, remote, local));
+
+               fetched = true;
+           }
+       }
+   }
+}
diff --git a/src/meson.build b/src/meson.build
index 16cda3c..d071d0b 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -71,6 +71,9 @@ sources = [
    'data/sources/humble/HumbleGame.vala',
    'data/sources/humble/Trove.vala',

+   'data/sources/indiegala/IndieGala.vala',
+   'data/sources/indiegala/IndieGalaGame.vala',
+
    'data/sources/itch/Itch.vala',
    'data/sources/itch/ItchGame.vala',
    'data/sources/itch/ItchDownloader.vala',
diff --git a/src/settings/Auth.vala b/src/settings/Auth.vala
index fa0b91c..d4bd7e3 100644
--- a/src/settings/Auth.vala
+++ b/src/settings/Auth.vala
@@ -109,6 +109,30 @@ namespace GameHub.Settings.Auth
        }
    }

+   public class IndieGala: SettingsSchema
+   {
+       public bool enabled { get; set; }
+       public bool authenticated { get; set; }
+
+       public IndieGala()
+       {
+           base(ProjectConfig.PROJECT_NAME + ".auth.indiegala");
+       }
+
+       private static IndieGala? _instance;
+       public static unowned IndieGala instance
+       {
+           get
+           {
+               if(_instance == null)
+               {
+                   _instance = new IndieGala();
+               }
+               return _instance;
+           }
+       }
+   }
+
    public class Itch: SettingsSchema
    {
        public bool enabled { get; set; }
diff --git a/src/settings/Paths.vala b/src/settings/Paths.vala
index 7582069..4383373 100644
--- a/src/settings/Paths.vala
+++ b/src/settings/Paths.vala
@@ -94,6 +94,30 @@ namespace GameHub.Settings.Paths
        }
    }

+   public class IndieGala: GameHub.Settings.SettingsSchema
+   {
+       public string[] game_directories { get; set; }
+       public string default_game_directory { get; set; }
+
+       public IndieGala()
+       {
+           base(ProjectConfig.PROJECT_NAME + ".paths.indiegala");
+       }
+
+       private static IndieGala _instance;
+       public static IndieGala instance
+       {
+           get
+           {
+               if(_instance == null)
+               {
+                   _instance = new IndieGala();
+               }
+               return _instance;
+           }
+       }
+   }
+
    public class Itch: GameHub.Settings.SettingsSchema
    {
        public string home { get; set; }
@@ -271,5 +295,50 @@ namespace GameHub.Settings.Paths
                }
            }
        }
+
+       public class IndieGala: GameHub.Settings.SettingsSchema
+       {
+           public string game_dir { get; set; }
+           public string installers { get; set; }
+
+           public static string expand_game_dir(string game, Platform? platform=null)
+           {
+               var g = game.replace(": ", " - ").replace(":", "");
+               var variables = new HashMap<string, string>();
+               variables.set("root", Collection.instance.root);
+               variables.set("game", g);
+               variables.set("platform_name", platform == null ? "." : platform.name());
+               variables.set("platform", platform == null ? "." : platform.id());
+               return FS.expand(instance.game_dir, null, variables);
+           }
+           public static string expand_installers(string game, Platform? platform=null)
+           {
+               var g = game.replace(": ", " - ").replace(":", "");
+               var variables = new HashMap<string, string>();
+               variables.set("root", Collection.instance.root);
+               variables.set("platform_name", platform == null ? "." : platform.name());
+               variables.set("platform", platform == null ? "." : platform.id());
+               variables.set("game_dir", expand_game_dir(g, platform));
+               return FS.expand(instance.installers, null, variables);
+           }
+
+           public IndieGala()
+           {
+               base(ProjectConfig.PROJECT_NAME + ".paths.collection.indiegala");
+           }
+
+           private static IndieGala? _instance;
+           public static unowned IndieGala instance
+           {
+               get
+               {
+                   if(_instance == null)
+                   {
+                       _instance = new IndieGala();
+                   }
+                   return _instance;
+               }
+           }
+       }
    }
 }
Protektor-Desura commented 3 years ago

IndieGala now has a new client in beta. I don't know if they have open sourced it or not though. Also not sure if there is anything other than Windows version available.

https://content.indiegalacdn.com/common/IGClientSetup.exe

https://docs.indiegala.com/gala_client/gala_client.html