Skip to content

Commit 91c4f13

Browse files
committed
feat: optional import extras from gog and humble
1 parent bb623e5 commit 91c4f13

File tree

14 files changed

+232
-35
lines changed

14 files changed

+232
-35
lines changed

source/Libraries/GogLibrary/GogLibrary.cs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,45 @@ internal List<GameMetadata> GetLibraryGames()
298298
Logger.Warn("Failed to obtain library stats data.");
299299
}
300300

301-
return LibraryGamesToGames(libGames).ToList();
301+
var result = LibraryGamesToGames(libGames).ToList();
302+
303+
if (SettingsViewModel.Settings.ImportGameExtras)
304+
{
305+
var extras = new List<GameMetadata>();
306+
foreach (var game in result)
307+
{
308+
try
309+
{
310+
var gogExtras = api.GetOwnedGameDetails(game.GameId).Extras;
311+
foreach (var x in gogExtras)
312+
{
313+
var id = x.ManualUrl.Split('/').Last();
314+
if (!long.TryParse(id, out _))
315+
{
316+
continue;
317+
}
318+
319+
var extraAsGame = new GameMetadata()
320+
{
321+
GameId = $"{game.GameId}_{id}",
322+
Source = new MetadataNameProperty("GOG"),
323+
Name = $"{game.Name} {x.Name.RemoveTrademarks()}",
324+
Categories = new HashSet<MetadataProperty>(){new MetadataNameProperty("extras")}, // TODO customizable category across plugins?
325+
Links = new List<Link>(){new Link("Download_me", $"https://www.gog.com{x.ManualUrl}")} // TODO use install action to launch browser or DL with playnite itself?
326+
// TODO any way to ignore post-actions from other plugins, eg search for metadata?
327+
};
328+
extras.Add(extraAsGame);
329+
}
330+
}
331+
catch (Exception e)
332+
{
333+
logger.Error(e, $"Failed to get extras for gog game: {game.GameId} {game.Name}.");
334+
}
335+
}
336+
result.AddRange(extras);
337+
}
338+
339+
return result;
302340
}
303341
}
304342

source/Libraries/GogLibrary/GogLibrary.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@
177177
<Compile Include="Models\GogGameTaskInfo.cs" />
178178
<Compile Include="Models\GetOwnedGamesResult.cs" />
179179
<Compile Include="Models\LibraryGameResponse.cs" />
180+
<Compile Include="Models\OwnedGameDetailsResponse.cs" />
180181
<Compile Include="Models\PagedResponse.cs" />
181182
<Compile Include="Models\ProductApiDetail.cs" />
182183
<Compile Include="Models\StoreGamesFilteredListResponse.cs" />

source/Libraries/GogLibrary/GogLibrarySettingsView.xaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<UserControl x:Class="GogLibrary.GogLibrarySettingsView"
22
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
33
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4-
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
5-
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
4+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
5+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
66
xmlns:local="clr-namespace:GogLibrary"
77
xmlns:sys="clr-namespace:System;assembly=mscorlib"
88
xmlns:pcon="clr-namespace:Playnite.Converters"
@@ -36,7 +36,7 @@
3636
Content="{DynamicResource LOCGOGSettingsImportUninstalledLabel}"/>
3737

3838
<StackPanel Orientation="Horizontal" Margin="0,10,0,10">
39-
<Button Content="{DynamicResource LOCGOGAuthenticateLabel}" HorizontalAlignment="Left"
39+
<Button Content="{DynamicResource LOCGOGAuthenticateLabel}" HorizontalAlignment="Left"
4040
Command="{Binding LoginCommand}" Margin="0,5,5,5"/>
4141
<TextBlock VerticalAlignment="Center" Margin="10,5,5,5">
4242
<TextBlock.Tag>
@@ -72,6 +72,8 @@
7272
Visibility="{Binding IsFirstRunUse, Converter={pcon:InvertedBooleanToVisibilityConverter}}"/>
7373
<CheckBox IsChecked="{Binding Settings.UseVerticalCovers}" DockPanel.Dock="Top"
7474
Content="{DynamicResource LOCGOGSettingsUseVerticalCovers}"/>
75+
<CheckBox IsChecked="{Binding Settings.ImportGameExtras}" DockPanel.Dock="Top"
76+
Content="{DynamicResource LOCGOGSettingsImportGameExtras}"/>
7577

7678
<StackPanel Orientation="Horizontal" Margin="0,10,0,0"
7779
Visibility="{Binding IsFirstRunUse, Converter={pcon:InvertedBooleanToVisibilityConverter}}">

source/Libraries/GogLibrary/GogLibrarySettingsViewModel.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class GogLibrarySettings
1414
public bool StartGamesUsingGalaxy { get; set; } = false;
1515
public bool UseAutomaticGameInstalls { get; set; } = false;
1616
public bool UseVerticalCovers { get; set; } = true;
17+
public bool ImportGameExtras { get; set; } = false;
1718
public string Locale { get; set; } = "en";
1819
}
1920
public class GogLibrarySettingsViewModel : PluginSettingsViewModel<GogLibrarySettings, GogLibrary>

source/Libraries/GogLibrary/Localization/en_US.xaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@
2323
<!--GOG-->
2424
<sys:String x:Key="LOCGOGUseLogin">Account Login</sys:String>
2525
<sys:String x:Key="LOCGOGUseAccountName">Account Name</sys:String>
26-
<sys:String x:Key="LOCGOGAccountSyncDescription" xml:space="preserve">Login to your account or use your public account name to sync your full library.
26+
<sys:String x:Key="LOCGOGAccountSyncDescription" xml:space="preserve">Login to your account or use your public account name to sync your full library.
2727
Note that the Account Name option only works for accounts whose game libraries have been set in GOG's privacy settings to be publicly visible.</sys:String>
2828
<sys:String x:Key="LOCSettingsGOGUseGalaxy">Launch games using GOG Galaxy client</sys:String>
2929
<sys:String x:Key="LOCSettingsGOGUseGalaxyTooltip">This will enable GOG Galaxy features like cloud saves, game time tracking, etc.</sys:String>
3030
<sys:String x:Key="LOCGOGSettingsUseAutomaticGameInstalls">Automatically initiate the install process when installing games on GOG Galaxy</sys:String>
31-
<sys:String x:Key="LOCGOGSettingsUseAutomaticGameInstallsTooltip" xml:space="preserve">If enabled, the install process will be automatically started without needing user interaction to the configured default install location.
31+
<sys:String x:Key="LOCGOGSettingsUseAutomaticGameInstallsTooltip" xml:space="preserve">If enabled, the install process will be automatically started without needing user interaction to the configured default install location.
3232
If disabled, the game page will be opened to manually start the install process.</sys:String>
3333
<sys:String x:Key="LOCGOGSettingsUseVerticalCovers">Use vertical covers</sys:String>
34-
</ResourceDictionary>
34+
<sys:String x:Key="LOCGOGSettingsImportGameExtras">Also import game extras</sys:String>
35+
</ResourceDictionary>

source/Libraries/GogLibrary/LocalizationKeys.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
///
22
/// DO NOT MODIFY! Automatically generated via UpdateLocExtFiles.ps1 script.
3-
///
3+
///
44
namespace System
55
{
66
public static class LOC
@@ -121,5 +121,9 @@ public static class LOC
121121
/// Use vertical covers
122122
/// </summary>
123123
public const string GOGSettingsUseVerticalCovers = "LOCGOGSettingsUseVerticalCovers";
124+
/// <summary>
125+
/// Import game extras (soundtracks, artwork, guides, ...) when importing games
126+
/// </summary>
127+
public const string GOGSettingsImportGameExtras = "LOCGOGSettingsImportGameExtras";
124128
}
125129
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace GogLibrary.Models
5+
{
6+
public class OwnedGameDetailsResponse
7+
{
8+
/// <example>The Witcher 3: Wild Hunt - Complete Edition</example>>
9+
public string Title { get; set; }
10+
11+
public List<Extra> Extras { get; set; }
12+
}
13+
14+
public class Extra
15+
{
16+
/// <example>/downloads/the_witcher_3_wild_hunt_game_of_the_year_edition_game/70003</example>
17+
public string ManualUrl { get; set; }
18+
19+
/// <example>soundtrack (MP3)</example>
20+
public string Name { get; set; }
21+
22+
/// <example>audio</example>
23+
public string Type { get; set; }
24+
25+
/// <summary>
26+
/// number of items in downloaded archive, displayed in UI like "paper toys (6)". not shown when equals 1 or 2.
27+
/// </summary>
28+
/// <example>1</example>
29+
public long Info { get; set; }
30+
31+
/// <example>206 MB</example>
32+
/// <example>4096 MB</example>
33+
public string Size { get; set; }
34+
}
35+
}

source/Libraries/GogLibrary/Services/GogAccountClient.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,5 +151,13 @@ public List<LibraryGameResponse> GetOwnedGames()
151151

152152
return games;
153153
}
154+
155+
public OwnedGameDetailsResponse GetOwnedGameDetails(string gameId)
156+
{
157+
webView.NavigateAndWait($@"https://www.gog.com/account/gameDetails/{gameId}.json");
158+
var stringInfo = webView.GetPageText();
159+
var response = Serialization.FromJson<OwnedGameDetailsResponse>(stringInfo);
160+
return response;
161+
}
154162
}
155163
}

source/Libraries/HumbleLibrary/HumbleLibrary.cs

Lines changed: 109 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -171,24 +171,8 @@ public List<ImportableTroveGame> GetTroveGames()
171171
return games.OrderBy(a => a.Name).ToList();
172172
}
173173

174-
public List<Order.SubProduct> GetLibraryGames()
174+
public List<Order.SubProduct> GetLibraryGames(List<Order> orders)
175175
{
176-
var libraryGames = new List<Game>();
177-
var orders = new List<Order>();
178-
using (var view = PlayniteApi.WebViews.CreateOffscreenView(
179-
new WebViewSettings
180-
{
181-
JavaScriptEnabled = false,
182-
UserAgent = UserAgent
183-
}))
184-
{
185-
var api = new HumbleAccountClient(view);
186-
var keys = api.GetLibraryKeys();
187-
orders = api.GetOrders(keys);
188-
}
189-
190-
// Humble will return null items in library response on some accounts
191-
orders = orders.Where(a => a != null).ToList();
192176
var selectedProducts = new List<Order.SubProduct>();
193177
var allTpks = orders.SelectMany(a => a.tpkd_dict?.all_tpks).ToList();
194178

@@ -230,6 +214,97 @@ public List<ImportableTroveGame> GetTroveGames()
230214
return selectedProducts;
231215
}
232216

217+
public List<GameMetadata> GetLibraryExtras(List<Order> orders)
218+
{
219+
var extras = new List<GameMetadata>();
220+
foreach (var order in orders)
221+
{
222+
var gameKey = order.gamekey;
223+
if (!order.subproducts.HasItems())
224+
{
225+
continue;
226+
}
227+
228+
foreach (var product in order.subproducts)
229+
{
230+
var productName = product.human_name.RemoveTrademarks();
231+
if (!product.downloads.HasItems())
232+
{
233+
continue;
234+
}
235+
236+
foreach (var download in product.downloads)
237+
{
238+
var downloadName = download.machine_name;
239+
if (!download.download_struct.HasItems())
240+
{
241+
continue;
242+
}
243+
244+
switch (download.platform)
245+
{
246+
case "windows":
247+
case "mac":
248+
case "linux":
249+
continue;
250+
case "asmjs":
251+
var extraAsGame = new GameMetadata()
252+
{
253+
GameId = $"{productName}_{downloadName}",
254+
Source = new MetadataNameProperty("Humble"),
255+
Name = $"{productName} asm.js version",
256+
Categories = new HashSet<MetadataProperty>(){new MetadataNameProperty("extras")}, // TODO customizable category across plugins?
257+
Links = new List<Link>(){new Link("Run_me", $"https://www.humblebundle.com/play/asmjs/{downloadName}/{gameKey}")} // TODO use install action to launch browser?
258+
// TODO any way to ignore post-actions from other plugins, eg search for metadata?
259+
};
260+
extras.Add(extraAsGame);
261+
continue;
262+
}
263+
264+
foreach (var actualDownload in download.download_struct)
265+
{
266+
var url = actualDownload.url.web;
267+
if (string.IsNullOrWhiteSpace(url))
268+
{
269+
continue;
270+
}
271+
var extraAsGame = new GameMetadata()
272+
{
273+
GameId = $"{productName}_{downloadName}_{actualDownload.name}",
274+
Source = new MetadataNameProperty("Humble"),
275+
Name = $"{productName} {download.platform} {actualDownload.name}",
276+
Categories = new HashSet<MetadataProperty>(){new MetadataNameProperty("extras")}, // TODO customizable category across plugins?
277+
Links = new List<Link>(){new Link("Download_me", url)} // TODO use install action to launch browser or DL with playnite itself?
278+
// TODO any way to ignore post-actions from other plugins, eg search for metadata?
279+
};
280+
extras.Add(extraAsGame);
281+
}
282+
283+
284+
}
285+
}
286+
}
287+
288+
return extras;
289+
}
290+
291+
private List<Order> GetAllOrders()
292+
{
293+
using (var view = PlayniteApi.WebViews.CreateOffscreenView(
294+
new WebViewSettings
295+
{
296+
JavaScriptEnabled = false,
297+
UserAgent = UserAgent
298+
}))
299+
{
300+
var api = new HumbleAccountClient(view);
301+
var keys = api.GetLibraryKeys();
302+
var orders = api.GetOrders(keys);
303+
// Humble will return null items in library response on some accounts
304+
return orders.Where(a => a != null).ToList();
305+
}
306+
}
307+
233308
public override IEnumerable<Game> ImportGames(LibraryImportGamesArgs args)
234309
{
235310
var importedGames = new List<Game>();
@@ -245,8 +320,9 @@ public override IEnumerable<Game> ImportGames(LibraryImportGamesArgs args)
245320
{
246321
if (SettingsViewModel.Settings.ImportGeneralLibrary)
247322
{
248-
var selectedProducts = GetLibraryGames();
249-
foreach (var product in selectedProducts)
323+
var orders = GetAllOrders();
324+
var gameProducts = GetLibraryGames(orders);
325+
foreach (var product in gameProducts)
250326
{
251327
var gameId = GetGameId(product);
252328
if (PlayniteApi.ApplicationSettings.GetGameExcludedFromImport(gameId, Id))
@@ -266,6 +342,19 @@ public override IEnumerable<Game> ImportGames(LibraryImportGamesArgs args)
266342
}, this));
267343
}
268344
}
345+
346+
if (SettingsViewModel.Settings.ImportGameExtras)
347+
{
348+
var extras = GetLibraryExtras(orders);
349+
foreach (var extra in extras)
350+
{
351+
var alreadyImported = PlayniteApi.Database.Games.FirstOrDefault(a => a.PluginId == Id && a.GameId == extra.GameId);
352+
if (alreadyImported == null)
353+
{
354+
importedGames.Add(PlayniteApi.Database.ImportGame(extra, this));
355+
}
356+
}
357+
}
269358
}
270359
}
271360
}
@@ -420,4 +509,4 @@ public override IEnumerable<UninstallController> GetUninstallActions(GetUninstal
420509
yield return new HumbleUninstallController(args.Game, this);
421510
}
422511
}
423-
}
512+
}

source/Libraries/HumbleLibrary/HumbleLibrarySettingsView.xaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
IsChecked="{Binding Settings.ImportGeneralLibrary}"
2929
Content="{DynamicResource LOCHumbleGeneralLibrary}"
3030
ToolTip="{DynamicResource LOCHumbleGeneralLibraryTooltip}" />
31+
<CheckBox Margin="0,10,0,0" Name="HumbleImportGameExtras"
32+
IsChecked="{Binding Settings.ImportGameExtras}"
33+
Content="{DynamicResource LOCHumbleImportGameExtras}"
34+
ToolTip="{DynamicResource LOCHumbleImportGameExtrasTooltip}" />
3135
<StackPanel IsEnabled="{Binding IsChecked, ElementName=HumbleImportGeneralLibrary}">
3236
<CheckBox Margin="20,10,0,0" Name="HumbleThirdPartyImport"
3337
IsChecked="{Binding Settings.IgnoreThirdPartyStoreGames}"
@@ -40,7 +44,7 @@
4044
ToolTip="{DynamicResource LOCHumbleImportThirdPartyDrmFreeTooltip}"/>
4145
</StackPanel>
4246
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
43-
<Button Content="{DynamicResource LOCHumbleAuthenticateLabel}" HorizontalAlignment="Left"
47+
<Button Content="{DynamicResource LOCHumbleAuthenticateLabel}" HorizontalAlignment="Left"
4448
Command="{Binding LoginCommand}" Margin="0,5,5,5"/>
4549
<TextBlock VerticalAlignment="Center" Margin="10,5,5,5">
4650
<TextBlock.Tag>
@@ -81,4 +85,4 @@
8185
</Hyperlink>
8286
</TextBlock>
8387
</StackPanel>
84-
</UserControl>
88+
</UserControl>

0 commit comments

Comments
 (0)