Skip to content

Commit 5598396

Browse files
committed
SteamScreenshots:
- Refactor and rewrite extension - Improved memory usage by loading low resolution thumbnail - Made more resilient to errors - Support for adding additional screenshot providers - Decoupling of services - Load images lazily in code behind rather than in converter
1 parent d7eec12 commit 5598396

21 files changed

+1125
-326
lines changed

source/Common/SteamCommon/Common.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ namespace SteamCommon
1414
{
1515
public static class Steam
1616
{
17-
private static ILogger logger = LogManager.GetLogger();
18-
private static Guid steamPluginId = Guid.Parse("cb91dfc9-b977-43bf-8e70-55f46e410fab");
19-
private static readonly Regex steamLinkRegex = new Regex(@"^https?:\/\/store\.steampowered\.com\/app\/(\d+)", RegexOptions.None);
17+
private static Guid _steamPluginId = Guid.Parse("cb91dfc9-b977-43bf-8e70-55f46e410fab");
18+
private static readonly Regex _steamLinkRegex = new Regex(@"^https?:\/\/store\.steampowered\.com\/app\/(\d+)", RegexOptions.None);
2019

2120
public static string GetGameSteamId(Game game, bool useLinksDetection = false)
2221
{
@@ -32,9 +31,9 @@ public static string GetGameSteamId(Game game, bool useLinksDetection = false)
3231
return null;
3332
}
3433

35-
private static string GetSteamIdFromLinks(Game game)
34+
public static string GetSteamIdFromLinks(Game game)
3635
{
37-
if (game.Links is null)
36+
if (game.Links is null || !game.Links.Any())
3837
{
3938
return null;
4039
}
@@ -46,7 +45,7 @@ private static string GetSteamIdFromLinks(Game game)
4645
continue;
4746
}
4847

49-
var linkMatch = steamLinkRegex.Match(gameLink.Url);
48+
var linkMatch = _steamLinkRegex.Match(gameLink.Url);
5049
if (linkMatch.Success)
5150
{
5251
return linkMatch.Groups[1].Value;
@@ -58,7 +57,7 @@ private static string GetSteamIdFromLinks(Game game)
5857

5958
public static bool IsGameSteamGame(Game game)
6059
{
61-
return game.PluginId == steamPluginId;
60+
return game.PluginId == _steamPluginId;
6261
}
6362

6463
public static string GetSteamApiMatchingLanguage(string playniteLanguage)
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using Playnite.SDK;
2+
using SteamScreenshots.Domain.Enums;
3+
using SteamScreenshots.Domain.Interfaces;
4+
using SteamScreenshots.Domain.ValueObjects;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using System.Text;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
12+
namespace SteamScreenshots.Application.Services
13+
{
14+
public class ScreenshotManagementService : IDisposable
15+
{
16+
private bool _disposed = false;
17+
private readonly IDictionary<ScreenshotServiceType, IScreenshotProvider> _providers;
18+
private readonly IImageProvider _imageProvider;
19+
private readonly ILogger _logger;
20+
private SemaphoreSlim _semaphore;
21+
22+
public ScreenshotManagementService(IDictionary<ScreenshotServiceType, IScreenshotProvider> providers, IImageProvider imageProvider, ILogger logger)
23+
{
24+
_providers = providers;
25+
_imageProvider = imageProvider;
26+
_logger = logger;
27+
_semaphore = new SemaphoreSlim(4);
28+
}
29+
30+
public async Task<List<Screenshot>> GetScreenshots(ScreenshotServiceType serviceType,
31+
string id,
32+
ScreenshotInitializationOptions screenshotInitializationOptions,
33+
CancellationToken cancellationToken = default)
34+
{
35+
if (!_providers.ContainsKey(serviceType))
36+
{
37+
throw new NotSupportedException($"Screenshot service {serviceType} is not supported.");
38+
}
39+
40+
var screenshotsData = _providers[serviceType].GetScreenshots(id, cancellationToken);
41+
if (!screenshotsData.HasItems())
42+
{
43+
return new List<Screenshot>();
44+
}
45+
46+
var screenshots = screenshotsData
47+
.Select(s => new Screenshot(s.ThumbnailUrl, s.FullImageUrl, _imageProvider))
48+
.ToList();
49+
50+
var initializeTasks = new List<Task>();
51+
foreach (var screenshot in screenshots)
52+
{
53+
if (!screenshotInitializationOptions.LazyLoadThumbnail)
54+
{
55+
initializeTasks.Add(InitializeWithSemaphoreAsync(screenshot.InitializeThumbnail, _semaphore));
56+
}
57+
58+
if (!screenshotInitializationOptions.LazyLoadFullImage)
59+
{
60+
initializeTasks.Add(InitializeWithSemaphoreAsync(screenshot.InitializeFullImage, _semaphore));
61+
}
62+
}
63+
64+
if (initializeTasks.Any())
65+
{
66+
// Initialize at least the first image so it's not loaded synchronously when first displayed
67+
if (screenshotInitializationOptions.LazyLoadFullImage)
68+
{
69+
initializeTasks.Add(InitializeWithSemaphoreAsync(screenshots[0].InitializeFullImage, _semaphore));
70+
}
71+
72+
await Task.WhenAll(initializeTasks);
73+
}
74+
75+
return screenshots;
76+
}
77+
78+
private async Task InitializeWithSemaphoreAsync(Action initializeFunc, SemaphoreSlim semaphore)
79+
{
80+
await semaphore.WaitAsync();
81+
try
82+
{
83+
await Task.Run(() => initializeFunc());
84+
}
85+
finally
86+
{
87+
semaphore.Release();
88+
}
89+
}
90+
91+
public void Dispose()
92+
{
93+
if (!_disposed)
94+
{
95+
_semaphore.Dispose();
96+
_semaphore = null;
97+
_disposed = true;
98+
}
99+
}
100+
101+
}
102+
103+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using FlowHttp;
2+
using Playnite.SDK.Data;
3+
using PluginsCommon;
4+
using SteamCommon.Models;
5+
using SteamScreenshots.Domain.Interfaces;
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Text;
10+
using System.Threading;
11+
using System.Threading.Tasks;
12+
13+
namespace SteamScreenshots.Application.Services
14+
{
15+
public class SteamAppDetailsService : ISteamAppDetailsService
16+
{
17+
private readonly ISteamAppDetailsRepository _appDetailsRepository;
18+
19+
public SteamAppDetailsService(ISteamAppDetailsRepository appDetailsRepository)
20+
{
21+
_appDetailsRepository = appDetailsRepository;
22+
}
23+
24+
public SteamAppDetails GetAppDetails(string id, bool downloadFromNetwork, CancellationToken cancellationToken = default)
25+
{
26+
if (!downloadFromNetwork)
27+
{
28+
return _appDetailsRepository.GetAppDetails(id);
29+
}
30+
31+
var existingDataCreationDate = _appDetailsRepository.GetAppDetailsCreationDate(id);
32+
if (!existingDataCreationDate.HasValue || existingDataCreationDate.Value < DateTime.Now.AddDays(-12))
33+
{
34+
var url = string.Format(@"https://store.steampowered.com/api/appdetails?appids={0}", id);
35+
var result = HttpRequestFactory.GetHttpRequest()
36+
.WithUrl(url)
37+
.DownloadString(cancellationToken);
38+
39+
if (result.IsSuccess &&
40+
Serialization.TryFromJson<Dictionary<string, SteamAppDetails>>(result.Content, out var parsedData)
41+
&& parsedData.Keys?.Any() == true)
42+
{
43+
var appDetails = parsedData[parsedData.Keys.First()];
44+
SaveAppDetails(id, appDetails);
45+
return appDetails;
46+
}
47+
}
48+
49+
return _appDetailsRepository.GetAppDetails(id);
50+
}
51+
52+
public void SaveAppDetails(string id, SteamAppDetails details)
53+
{
54+
_appDetailsRepository.SaveAppDetails(id, details);
55+
}
56+
57+
public void DeleteAppDetails(string id)
58+
{
59+
_appDetailsRepository.DeleteAppDetails(id);
60+
}
61+
}
62+
63+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace SteamScreenshots.Domain.Enums
8+
{
9+
public enum ScreenshotServiceType
10+
{
11+
Steam
12+
}
13+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using System.Windows.Media.Imaging;
7+
8+
namespace SteamScreenshots.Domain.Interfaces
9+
{
10+
public interface IImageProvider
11+
{
12+
BitmapImage LoadImage(string path);
13+
BitmapImage LoadImageWithDecodeMaxDimensions(string url, int decodeMaxWidth = 0, int decodeMaxHeight = 0);
14+
}
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using SteamScreenshots.Domain.ValueObjects;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace SteamScreenshots.Domain.Interfaces
10+
{
11+
public interface IScreenshotProvider
12+
{
13+
List<ScreenshotData> GetScreenshots(string id, CancellationToken cancellationToken = default);
14+
}
15+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using SteamCommon.Models;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
8+
namespace SteamScreenshots.Domain.Interfaces
9+
{
10+
public interface ISteamAppDetailsRepository
11+
{
12+
SteamAppDetails GetAppDetails(string id);
13+
void SaveAppDetails(string id, SteamAppDetails details);
14+
void DeleteAppDetails(string id);
15+
DateTime? GetAppDetailsCreationDate(string id);
16+
}
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using SteamCommon.Models;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace SteamScreenshots.Domain.Interfaces
10+
{
11+
public interface ISteamAppDetailsService
12+
{
13+
SteamAppDetails GetAppDetails(string id, bool downloadFromNetwork, CancellationToken cancellationToken = default);
14+
void SaveAppDetails(string id, SteamAppDetails details);
15+
void DeleteAppDetails(string id);
16+
}
17+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using PluginsCommon;
2+
using SteamScreenshots.Domain.Interfaces;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using System.Windows.Media.Imaging;
10+
11+
namespace SteamScreenshots.Domain.ValueObjects
12+
{
13+
public class Screenshot
14+
{
15+
private readonly string _thumbnailPath;
16+
private readonly string _fullImagePath;
17+
private readonly IImageProvider _imageProvider;
18+
19+
private readonly Lazy<BitmapImage> _lazyThumbnail;
20+
private readonly Lazy<BitmapImage> _lazyFullImage;
21+
22+
public BitmapImage ThumbnailImage => _lazyThumbnail.Value;
23+
public BitmapImage FullImage => _lazyFullImage.Value;
24+
25+
public Screenshot(string thumbnailPath, string fullImagePath, IImageProvider imageProvider)
26+
{
27+
_thumbnailPath = thumbnailPath;
28+
_fullImagePath = fullImagePath;
29+
_imageProvider = imageProvider;
30+
_lazyThumbnail = new Lazy<BitmapImage>(LoadThumbnail, LazyThreadSafetyMode.ExecutionAndPublication);
31+
_lazyFullImage = new Lazy<BitmapImage>(LoadFullImage, LazyThreadSafetyMode.ExecutionAndPublication);
32+
}
33+
34+
private BitmapImage LoadThumbnail()
35+
{
36+
if (!_thumbnailPath.IsNullOrEmpty())
37+
{
38+
return _imageProvider.LoadImageWithDecodeMaxDimensions(_thumbnailPath, 216, 216);
39+
}
40+
else
41+
{
42+
return _imageProvider.LoadImageWithDecodeMaxDimensions(_fullImagePath, 216, 216);
43+
}
44+
}
45+
46+
private BitmapImage LoadFullImage()
47+
{
48+
return _imageProvider.LoadImage(_fullImagePath);
49+
}
50+
51+
public void InitializeThumbnail()
52+
{
53+
if (_lazyThumbnail.IsValueCreated)
54+
{
55+
return;
56+
}
57+
58+
_ = _lazyThumbnail.Value;
59+
}
60+
61+
public void InitializeFullImage()
62+
{
63+
if (_lazyFullImage.IsValueCreated)
64+
{
65+
return;
66+
}
67+
68+
_ = _lazyFullImage.Value;
69+
}
70+
71+
public void InitializeImages()
72+
{
73+
InitializeThumbnail();
74+
InitializeFullImage();
75+
}
76+
}
77+
78+
}

0 commit comments

Comments
 (0)