Skip to content

Commit 2791023

Browse files
committed
Integrate Redlib for free game discovery, add configurations, strategies, error handling, tests and update build process
This commit introduces Redlib integration into the ASFFreeGames project, allowing it to fetch free games from Redlib instances. Key changes include: * **Redlib Integration:** * Added support for Redlib as a source for finding free games. * Implemented `RedlibListFreeGamesStrategy` to fetch games from Redlib instances. * Introduced configurations for Redlib proxy and instance URL. * Updated `ListFreeGamesMainStrategy` to handle fetching from Redlib as a fallback strategy. * **Code refactoring:** * Introduced `EListFreeGamesStrategy` enum to represent supported free game listing sources (Reddit, Redlib). * Improved logic for handling successful and failed attempts in `ListFreeGamesMainStrategy`. * Added exception handling for Redlib related issues. * **Testing:** * Added a new unit test (`RedlibInstanceListTests.Test`) to verify Redlib instance listing functionality. * Updated `FreeGamesCommand.Test` to handle Redlib strategy. * **Build:** * Added `Resouces` folder to the project. * Included `redlib_instances.json` as an embedded resource to store Redlib instances.
1 parent 78b042a commit 2791023

32 files changed

+995
-38
lines changed

ASFFreeGames.Tests/Redlib/RedlibHtmlParserTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Text;
55
using System.Threading.Tasks;
66
using Maxisoft.ASF.Redlib;
7+
using Maxisoft.ASF.Redlib.Html;
78
using Xunit;
89

910
namespace Maxisoft.ASF.Tests.Redlib;
@@ -14,7 +15,7 @@ public async void Test() {
1415
string html = await LoadHtml().ConfigureAwait(false);
1516

1617
// ReSharper disable once ArgumentsStyleLiteral
17-
IReadOnlyCollection<GameEntry> result = RedlibHtmlParser.ParseGamesFromHtml(html, dedup: false);
18+
IReadOnlyCollection<RedlibGameEntry> result = RedlibHtmlParser.ParseGamesFromHtml(html, dedup: false);
1819
Assert.NotEmpty(result);
1920
Assert.Equal(25, result.Count);
2021

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Reflection;
5+
using System.Text;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using ASFFreeGames.Configurations;
9+
using Maxisoft.ASF.Redlib;
10+
using Maxisoft.ASF.Redlib.Html;
11+
using Maxisoft.ASF.Redlib.Instances;
12+
using Xunit;
13+
14+
namespace Maxisoft.ASF.Tests.Redlib;
15+
16+
public class RedlibInstanceListTests {
17+
[Fact]
18+
public async void Test() {
19+
RedlibInstanceList lister = new(new ASFFreeGamesOptions());
20+
List<Uri> uris = await RedlibInstanceList.ListFromEmbedded(default(CancellationToken)).ConfigureAwait(false);
21+
22+
Assert.NotEmpty(uris);
23+
}
24+
}

ASFFreeGames/ASFFreeGames.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,13 @@
7272
<Link>Directory.Build.props</Link>
7373
</Content>
7474
</ItemGroup>
75+
76+
<ItemGroup>
77+
<Folder Include="Resouces\" />
78+
</ItemGroup>
79+
80+
<ItemGroup>
81+
<None Remove="Resouces\redlib_instances.json" />
82+
<EmbeddedResource Include="Resouces\redlib_instances.json" />
83+
</ItemGroup>
7584
</Project>

ASFFreeGames/Commands/FreeGamesCommand.cs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Maxisoft.ASF;
1515
using Maxisoft.ASF.ASFExtentions;
1616
using Maxisoft.ASF.Configurations;
17+
using Maxisoft.ASF.FreeGames.Strategies;
1718
using Maxisoft.ASF.HttpClientSimple;
1819
using Maxisoft.ASF.Reddit;
1920
using Maxisoft.ASF.Utils;
@@ -23,6 +24,8 @@ namespace ASFFreeGames.Commands {
2324
// Implement the IBotCommand interface
2425
internal sealed class FreeGamesCommand(ASFFreeGamesOptions options) : IBotCommand, IDisposable {
2526
public void Dispose() {
27+
Strategy.Dispose();
28+
2629
if (HttpFactory.IsValueCreated) {
2730
HttpFactory.Value.Dispose();
2831
}
@@ -40,6 +43,9 @@ public void Dispose() {
4043

4144
private readonly Lazy<SimpleHttpClientFactory> HttpFactory = new(() => new SimpleHttpClientFactory(options));
4245

46+
public IListFreeGamesStrategy Strategy { get; internal set; } = new ListFreeGamesMainStrategy();
47+
public EListFreeGamesStrategy PreviousSucessfulStrategy { get; private set; } = EListFreeGamesStrategy.Reddit | EListFreeGamesStrategy.Redlib;
48+
4349
// Define a constructor that takes an plugin options instance as a parameter
4450

4551
/// <inheritdoc />
@@ -218,23 +224,39 @@ private async Task<int> CollectGames(IEnumerable<Bot> bots, ECollectGameRequestS
218224
try {
219225
IReadOnlyCollection<RedditGameEntry> games;
220226

227+
ListFreeGamesContext strategyContext = new(Options, new Lazy<SimpleHttpClient>(() => HttpFactory.Value.CreateGeneric())) {
228+
Strategy = Strategy,
229+
HttpClientFactory = HttpFactory.Value,
230+
PreviousSucessfulStrategy = PreviousSucessfulStrategy
231+
};
232+
221233
try {
222234
#pragma warning disable CA2000
223-
games = await RedditHelper.GetGames(HttpFactory.Value.CreateForReddit(), cancellationToken).ConfigureAwait(false);
235+
games = await Strategy.GetGames(strategyContext, cancellationToken).ConfigureAwait(false);
224236
#pragma warning restore CA2000
225237
}
226238
catch (Exception e) when (e is InvalidOperationException or JsonException or IOException or RedditServerException) {
227239
if (Options.VerboseLog ?? false) {
228240
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericException(e);
229241
}
230242
else {
231-
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericError($"Unable to load json from reddit {e.GetType().Name}: {e.Message}");
243+
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericError($"Unable to get and load json {e.GetType().Name}: {e.Message}");
232244
}
233245

234246
return 0;
235247
}
248+
finally {
249+
PreviousSucessfulStrategy = strategyContext.PreviousSucessfulStrategy;
250+
251+
if (Options.VerboseLog ?? false) {
252+
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo($"PreviousSucessfulStrategy = {PreviousSucessfulStrategy}");
253+
}
254+
}
236255

237-
LogNewGameCount(games, VerboseLog || requestSource is ECollectGameRequestSource.RequestedByUser);
256+
#pragma warning disable CA1308
257+
string remote = strategyContext.PreviousSucessfulStrategy.ToString().ToLowerInvariant();
258+
#pragma warning restore CA1308
259+
LogNewGameCount(games, remote, VerboseLog || requestSource is ECollectGameRequestSource.RequestedByUser);
238260

239261
foreach (Bot bot in bots) {
240262
if (cancellationToken.IsCancellationRequested) {
@@ -348,7 +370,7 @@ private async Task<int> CollectGames(IEnumerable<Bot> bots, ECollectGameRequestS
348370
return res;
349371
}
350372

351-
private void LogNewGameCount(IReadOnlyCollection<RedditGameEntry> games, bool logZero = false) {
373+
private void LogNewGameCount(IReadOnlyCollection<RedditGameEntry> games, string remote, bool logZero = false) {
352374
int totalAppIdCounter = PreviouslySeenAppIds.Count;
353375
int newGameCounter = 0;
354376

@@ -359,13 +381,13 @@ private void LogNewGameCount(IReadOnlyCollection<RedditGameEntry> games, bool lo
359381
}
360382

361383
if ((totalAppIdCounter == 0) && (games.Count > 0)) {
362-
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo($"[FreeGames] found potentially {games.Count} free games on reddit", nameof(CollectGames));
384+
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo($"[FreeGames] found potentially {games.Count} free games on {remote}", nameof(CollectGames));
363385
}
364386
else if (newGameCounter > 0) {
365-
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo($"[FreeGames] found {newGameCounter} fresh free game(s) on reddit", nameof(CollectGames));
387+
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo($"[FreeGames] found {newGameCounter} fresh free game(s) on {remote}", nameof(CollectGames));
366388
}
367389
else if ((newGameCounter == 0) && logZero) {
368-
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo($"[FreeGames] found 0 new game out of {games.Count} free games on reddit", nameof(CollectGames));
390+
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo($"[FreeGames] found 0 new game out of {games.Count} free games on {remote}", nameof(CollectGames));
369391
}
370392
}
371393

ASFFreeGames/Configurations/ASFFreeGamesOptions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,13 @@ public bool IsBlacklisted(in GameIdentifier gid) {
5151

5252
[JsonPropertyName("redditProxy")]
5353
public string? RedditProxy { get; set; }
54+
55+
[JsonPropertyName("redlibProxy")]
56+
public string? RedlibProxy { get; set; }
5457
#endregion
58+
59+
[JsonPropertyName("redlibInstanceUrl")]
60+
#pragma warning disable CA1056
61+
public string? RedlibInstanceUrl { get; set; } = "https://raw.githubusercontent.com/redlib-org/redlib-instances/main/instances.json";
62+
#pragma warning restore CA1056
5563
}

ASFFreeGames/Configurations/ASFFreeGamesOptionsLoader.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public static void Bind(ref ASFFreeGamesOptions options) {
3131
options.RandomizeRecheckInterval = configurationRoot.GetValue("RandomizeRecheckInterval", options.RandomizeRecheckInterval);
3232
options.Proxy = configurationRoot.GetValue("Proxy", options.Proxy);
3333
options.RedditProxy = configurationRoot.GetValue("RedditProxy", options.RedditProxy);
34+
options.RedlibProxy = configurationRoot.GetValue("RedlibProxy", options.RedlibProxy);
35+
options.RedlibInstanceUrl = configurationRoot.GetValue("RedlibInstanceUrl", options.RedlibInstanceUrl);
3436
}
3537
finally {
3638
Semaphore.Release();

ASFFreeGames/Configurations/ASFFreeGamesOptionsSaver.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,12 @@ internal static int CreateOptionsBuffer(ASFFreeGamesOptions options, IMemoryOwne
4949
written += WriteNameAndProperty("verboseLog"u8, options.VerboseLog, buffer, written);
5050
written += WriteNameAndProperty("proxy"u8, options.Proxy, buffer, written);
5151
written += WriteNameAndProperty("redditProxy"u8, options.RedditProxy, buffer, written);
52+
written += WriteNameAndProperty("redlibProxy"u8, options.RedlibProxy, buffer, written);
53+
written += WriteNameAndProperty("redlibInstanceUrl"u8, options.RedlibInstanceUrl, buffer, written);
5254
RemoveTrailingCommaAndLineReturn(buffer, ref written);
5355

5456
written += WriteJsonString("\n}"u8, buffer, written);
5557

56-
// Resize buffer if needed
5758
if (written >= buffer.Length) {
5859
throw new InvalidOperationException("Buffer overflow while saving options");
5960
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System;
2+
3+
// ReSharper disable once CheckNamespace
4+
namespace Maxisoft.ASF.FreeGames.Strategies;
5+
6+
[Flags]
7+
public enum EListFreeGamesStrategy {
8+
None = 0,
9+
Reddit = 1 << 0,
10+
Redlib = 1 << 1
11+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
using System.Net;
3+
using Maxisoft.ASF.Redlib;
4+
5+
// ReSharper disable once CheckNamespace
6+
namespace Maxisoft.ASF.FreeGames.Strategies;
7+
8+
public class HttpRequestRedlibException : RedlibException {
9+
public required HttpStatusCode? StatusCode { get; init; }
10+
public required Uri? Uri { get; init; }
11+
12+
public HttpRequestRedlibException() { }
13+
public HttpRequestRedlibException(string message) : base(message) { }
14+
public HttpRequestRedlibException(string message, Exception inner) : base(message, inner) { }
15+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Maxisoft.ASF.Reddit;
7+
8+
// ReSharper disable once CheckNamespace
9+
namespace Maxisoft.ASF.FreeGames.Strategies;
10+
11+
[SuppressMessage("ReSharper", "RedundantNullableFlowAttribute")]
12+
public interface IListFreeGamesStrategy : IDisposable {
13+
Task<IReadOnlyCollection<RedditGameEntry>> GetGames([NotNull] ListFreeGamesContext context, CancellationToken cancellationToken);
14+
15+
public static Exception ExceptionFromTask<T>([NotNull] Task<T> task) {
16+
if (task is { IsFaulted: true, Exception: not null }) {
17+
return task.Exception.InnerExceptions.Count == 1 ? task.Exception.InnerExceptions[0] : task.Exception;
18+
}
19+
20+
if (task.IsCanceled) {
21+
return new TaskCanceledException();
22+
}
23+
24+
throw new InvalidOperationException("Unknown task state");
25+
}
26+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
using ASFFreeGames.Configurations;
3+
using Maxisoft.ASF.HttpClientSimple;
4+
5+
// ReSharper disable once CheckNamespace
6+
namespace Maxisoft.ASF.FreeGames.Strategies;
7+
8+
public sealed record ListFreeGamesContext(ASFFreeGamesOptions Options, Lazy<SimpleHttpClient> HttpClient, uint Retry = 5) {
9+
public required SimpleHttpClientFactory HttpClientFactory { get; init; }
10+
public EListFreeGamesStrategy PreviousSucessfulStrategy { get; set; }
11+
12+
public required IListFreeGamesStrategy Strategy { get; init; }
13+
}

0 commit comments

Comments
 (0)